Store width and height of images.

Use it to calculate width and height of small images in lists.

Since the old database should be migratable to the new, the width and
height fields are nullable.  I aim to make them not nullable later,
after all images in the database has got width and height.
This commit is contained in:
Rasmus Kaj 2018-03-05 00:29:36 +01:00
parent c2388befa9
commit fc1ed81561
10 changed files with 67 additions and 48 deletions

View File

@ -0,0 +1,2 @@
ALTER TABLE photos DROP COLUMN width;
ALTER TABLE photos DROP COLUMN height;

View File

@ -0,0 +1,6 @@
-- Add width and height to photos
-- Intially make them nullable, to make it possible to apply the
-- migration to an existing database. A NOT NULL constraint should
-- be added later, when all photos has sizes.
ALTER TABLE photos ADD COLUMN width INTEGER;
ALTER TABLE photos ADD COLUMN height INTEGER;

View File

@ -27,9 +27,15 @@ fn save_photo(
file_path: &str, file_path: &str,
exif: &ExifData, exif: &ExifData,
) -> Result<(), Error> { ) -> Result<(), Error> {
let width = exif.width
.ok_or(Error::Other(format!("Image {} missing width", file_path)))?;
let height = exif.height
.ok_or(Error::Other(format!("Image {} missing height", file_path)))?;
let photo = match Photo::create_or_set_basics( let photo = match Photo::create_or_set_basics(
db, db,
file_path, file_path,
width as i32,
height as i32,
exif.date(), exif.date(),
exif.rotation()?, exif.rotation()?,
find_camera(db, exif)?, find_camera(db, exif)?,

View File

@ -3,15 +3,10 @@ use diesel::pg::PgConnection;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::result::Error as DieselError; use diesel::result::Error as DieselError;
use diesel::update; use diesel::update;
use models::{Modification, Photo}; use models::Photo;
use photosdir::PhotosDir;
use std::io::prelude::*; use std::io::prelude::*;
pub fn one( pub fn one(db: &PgConnection, tpath: &str) -> Result<(), Error> {
db: &PgConnection,
photodir: &PhotosDir,
tpath: &str,
) -> Result<(), Error> {
use schema::photos::dsl::*; use schema::photos::dsl::*;
match update(photos.filter(path.eq(&tpath))) match update(photos.filter(path.eq(&tpath)))
.set(is_public.eq(true)) .set(is_public.eq(true))
@ -22,15 +17,7 @@ pub fn one(
Ok(()) Ok(())
} }
Err(DieselError::NotFound) => { Err(DieselError::NotFound) => {
if !photodir.has_file(&tpath) { Err(Error::Other(format!("File {} is not known", tpath)))
return Err(Error::Other(format!(
"File {} does not exist",
tpath,
)));
}
let photo = register_photo(db, tpath)?;
println!("New photo {:?} is public.", photo);
Ok(())
} }
Err(error) => Err(error.into()), Err(error) => Err(error.into()),
} }
@ -38,26 +25,10 @@ pub fn one(
pub fn by_file_list<In: BufRead + Sized>( pub fn by_file_list<In: BufRead + Sized>(
db: &PgConnection, db: &PgConnection,
photodir: &PhotosDir,
list: In, list: In,
) -> Result<(), Error> { ) -> Result<(), Error> {
for line in list.lines() { for line in list.lines() {
one(db, photodir, &line?)?; one(db, &line?)?;
} }
Ok(()) Ok(())
} }
fn register_photo(
db: &PgConnection,
tpath: &str,
) -> Result<Photo, DieselError> {
use schema::photos::dsl::{is_public, photos};
let photo = match Photo::create_or_set_basics(db, tpath, None, 0, None)? {
Modification::Created(photo)
| Modification::Updated(photo)
| Modification::Unchanged(photo) => photo,
};
update(photos.find(photo.id))
.set(is_public.eq(true))
.get_result::<Photo>(db)
}

View File

@ -169,22 +169,19 @@ fn run(args: &ArgMatches) -> Result<(), Error> {
Ok(()) Ok(())
} }
("makepublic", Some(args)) => { ("makepublic", Some(args)) => {
let pd = PhotosDir::new(photos_dir());
let db = get_db()?; let db = get_db()?;
match args.value_of("LIST") { match args.value_of("LIST") {
Some("-") => { Some("-") => {
let list = io::stdin(); let list = io::stdin();
makepublic::by_file_list(&db, &pd, list.lock())?; makepublic::by_file_list(&db, list.lock())?;
Ok(()) Ok(())
} }
Some(f) => { Some(f) => {
let list = File::open(f)?; let list = File::open(f)?;
let list = BufReader::new(list); let list = BufReader::new(list);
makepublic::by_file_list(&db, &pd, list) makepublic::by_file_list(&db, list)
}
None => {
makepublic::one(&db, &pd, args.value_of("IMAGE").unwrap())
} }
None => makepublic::one(&db, args.value_of("IMAGE").unwrap()),
} }
} }
("stats", Some(_args)) => show_stats(&get_db()?), ("stats", Some(_args)) => show_stats(&get_db()?),

View File

@ -4,6 +4,7 @@ use diesel::pg::PgConnection;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::result::Error as DieselError; use diesel::result::Error as DieselError;
use server::SizeTag; use server::SizeTag;
use std::cmp::max;
#[derive(AsChangeset, Clone, Debug, Identifiable, Queryable)] #[derive(AsChangeset, Clone, Debug, Identifiable, Queryable)]
pub struct Photo { pub struct Photo {
@ -15,6 +16,8 @@ pub struct Photo {
pub is_public: bool, pub is_public: bool,
pub camera_id: Option<i32>, pub camera_id: Option<i32>,
pub attribution_id: Option<i32>, pub attribution_id: Option<i32>,
pub width: Option<i32>,
pub height: Option<i32>,
} }
use schema::photos; use schema::photos;
@ -55,6 +58,8 @@ impl Photo {
pub fn update_by_path( pub fn update_by_path(
db: &PgConnection, db: &PgConnection,
file_path: &str, file_path: &str,
newwidth: i32,
newheight: i32,
exifdate: Option<NaiveDateTime>, exifdate: Option<NaiveDateTime>,
exifrotation: i16, exifrotation: i16,
camera: &Option<Camera>, camera: &Option<Camera>,
@ -69,6 +74,12 @@ impl Photo {
{ {
let mut change = false; let mut change = false;
// TODO Merge updates to one update statement! // TODO Merge updates to one update statement!
if pic.width != Some(newwidth) || pic.height != Some(newheight) {
change = true;
pic = diesel::update(photos.find(pic.id))
.set((width.eq(newwidth), height.eq(newheight)))
.get_result::<Photo>(db)?;
}
if exifdate.is_some() && exifdate != pic.date { if exifdate.is_some() && exifdate != pic.date {
change = true; change = true;
pic = diesel::update(photos.find(pic.id)) pic = diesel::update(photos.find(pic.id))
@ -102,6 +113,8 @@ impl Photo {
pub fn create_or_set_basics( pub fn create_or_set_basics(
db: &PgConnection, db: &PgConnection,
file_path: &str, file_path: &str,
newwidth: i32,
newheight: i32,
exifdate: Option<NaiveDateTime>, exifdate: Option<NaiveDateTime>,
exifrotation: i16, exifrotation: i16,
camera: Option<Camera>, camera: Option<Camera>,
@ -112,6 +125,8 @@ impl Photo {
if let Some(result) = Self::update_by_path( if let Some(result) = Self::update_by_path(
db, db,
file_path, file_path,
newwidth,
newheight,
exifdate, exifdate,
exifrotation, exifrotation,
&camera, &camera,
@ -123,6 +138,8 @@ impl Photo {
path.eq(file_path), path.eq(file_path),
date.eq(exifdate), date.eq(exifdate),
rotation.eq(exifrotation), rotation.eq(exifrotation),
width.eq(newwidth),
height.eq(newheight),
camera_id.eq(camera.map(|c| c.id)), camera_id.eq(camera.map(|c| c.id)),
)) ))
.get_result::<Photo>(db)?; .get_result::<Photo>(db)?;
@ -193,6 +210,19 @@ impl Photo {
use schema::cameras::dsl::cameras; use schema::cameras::dsl::cameras;
self.camera_id.and_then(|i| cameras.find(i).first(db).ok()) self.camera_id.and_then(|i| cameras.find(i).first(db).ok())
} }
pub fn get_size(&self, max_size: u32) -> Option<(u32, u32)> {
if let (Some(width), Some(height)) = (self.width, self.height) {
let scale = f64::from(max_size) / f64::from(max(width, height));
let w = (scale * f64::from(width)) as u32;
let h = (scale * f64::from(height)) as u32;
match self.rotation {
_x @ 0...44 | _x @ 315...360 | _x @ 135...224 => Some((w, h)),
_ => Some((h, w)),
}
} else {
None
}
}
} }
#[derive(Debug, Clone, Queryable)] #[derive(Debug, Clone, Queryable)]

View File

@ -14,8 +14,8 @@ pub struct ExifData {
gpstime: Option<(u8, u8, u8)>, gpstime: Option<(u8, u8, u8)>,
make: Option<String>, make: Option<String>,
model: Option<String>, model: Option<String>,
width: Option<u32>, pub width: Option<u32>,
height: Option<u32>, pub height: Option<u32>,
orientation: Option<u32>, orientation: Option<u32>,
latval: Option<f64>, latval: Option<f64>,
longval: Option<f64>, longval: Option<f64>,

View File

@ -37,6 +37,8 @@ pub struct PhotoLink {
pub title: Option<String>, pub title: Option<String>,
pub href: String, pub href: String,
pub id: i32, pub id: i32,
// Size should not be optional, but make it best-effort for now.
pub size: Option<(u32, u32)>,
pub lable: Option<String>, pub lable: Option<String>,
} }
@ -48,6 +50,7 @@ impl PhotoLink {
fn imgscore(p: &Photo) -> i16 { fn imgscore(p: &Photo) -> i16 {
p.grade.unwrap_or(27) + if p.is_public { 38 } else { 0 } p.grade.unwrap_or(27) + if p.is_public { 38 } else { 0 }
} }
let photo = g.iter().max_by_key(|p| imgscore(p)).unwrap();
PhotoLink { PhotoLink {
title: None, title: None,
href: format!( href: format!(
@ -56,10 +59,8 @@ impl PhotoLink {
g.last().map(|p| p.id).unwrap_or(0), g.last().map(|p| p.id).unwrap_or(0),
g.first().map(|p| p.id).unwrap_or(0), g.first().map(|p| p.id).unwrap_or(0),
), ),
id: g.iter() id: photo.id,
.max_by_key(|p| imgscore(p)) size: photo.get_size(SizeTag::Small.px()),
.map(|p| p.id)
.unwrap_or(0),
lable: { lable: {
let from = g.last().and_then(|p| p.date); let from = g.last().and_then(|p| p.date);
let to = g.first().and_then(|p| p.date); let to = g.first().and_then(|p| p.date);
@ -101,6 +102,7 @@ impl<'a> From<&'a Photo> for PhotoLink {
title: None, title: None,
href: format!("/img/{}", p.id), href: format!("/img/{}", p.id),
id: p.id, id: p.id,
size: p.get_size(SizeTag::Small.px()),
lable: p.date.map(|d| format!("{}", d.format("%F %T"))), lable: p.date.map(|d| format!("{}", d.format("%F %T"))),
} }
} }

View File

@ -1,4 +1,4 @@
use super::{Link, PhotoLink}; use super::{Link, PhotoLink, SizeTag};
use super::splitlist::links_by_time; use super::splitlist::links_by_time;
use chrono::Duration as ChDuration; use chrono::Duration as ChDuration;
use chrono::naive::{NaiveDate, NaiveDateTime}; use chrono::naive::{NaiveDate, NaiveDateTime};
@ -46,6 +46,7 @@ pub fn all_years<'mw>(
} else { } else {
q.filter(date.is_null()) q.filter(date.is_null())
}; };
let photo = photo.first::<Photo>(c).unwrap();
PhotoLink { PhotoLink {
title: Some( title: Some(
year.map(|y| format!("{}", y)) year.map(|y| format!("{}", y))
@ -53,7 +54,8 @@ pub fn all_years<'mw>(
), ),
href: format!("/{}/", year.unwrap_or(0)), href: format!("/{}/", year.unwrap_or(0)),
lable: Some(format!("{} images", count)), lable: Some(format!("{} images", count)),
id: photo.first::<Photo>(c).unwrap().id, id: photo.id,
size: photo.get_size(SizeTag::Small.px()),
} }
}) })
.collect(); .collect();
@ -105,6 +107,7 @@ pub fn months_in_year<'mw>(
href: format!("/{}/{}/", year, month), href: format!("/{}/{}/", year, month),
lable: Some(format!("{} pictures", count)), lable: Some(format!("{} pictures", count)),
id: photo.id, id: photo.id,
size: photo.get_size(SizeTag::Small.px()),
} }
}) })
.collect(); .collect();
@ -158,6 +161,7 @@ pub fn days_in_month<'mw>(
href: format!("/{}/{}/{}", year, month, day), href: format!("/{}/{}/{}", year, month, day),
lable: Some(format!("{} pictures", count)), lable: Some(format!("{} pictures", count)),
id: photo.id, id: photo.id,
size: photo.get_size(SizeTag::Small.px()),
} }
}) })
.collect(); .collect();
@ -278,6 +282,7 @@ pub fn on_this_day<'mw>(
href: format!("/{}/{}/{}", year, month, day), href: format!("/{}/{}/{}", year, month, day),
lable: Some(format!("{} pictures", count)), lable: Some(format!("{} pictures", count)),
id: photo.id, id: photo.id,
size: photo.get_size(SizeTag::Small.px()),
} }
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),

View File

@ -2,6 +2,6 @@
@(photo: &PhotoLink) @(photo: &PhotoLink)
<div class="item">@if let Some(ref title) = photo.title {<h2>@title</h2>} <div class="item">@if let Some(ref title) = photo.title {<h2>@title</h2>}
<a href="@photo.href"><img src="/img/@photo.id-s.jpg"></a> <a href="@photo.href"><img src="/img/@photo.id-s.jpg" @if let Some(s) = photo.size {width="@s.0" height="@s.1"} alt="Photo @photo.id"></a>
@if let Some(ref d) = photo.lable {<span class="lable">@d</span>} @if let Some(ref d) = photo.lable {<span class="lable">@d</span>}
</div> </div>