| @@ -125,7 +125,10 @@ pub struct ImageOp { | |||
| quality: u8, | |||
| /// Hash of the above parameters | |||
| hash: u64, | |||
| collision: Option<u32>, | |||
| /// If there is a hash collision with another ImageOp, this contains a sequential ID > 1 | |||
| /// identifying the collision in the order as encountered (which is essentially random). | |||
| /// Therefore, ImageOps with collisions (ie. collision_id > 0) are always considered out of date. | |||
| collision_id: u32, | |||
| } | |||
| impl ImageOp { | |||
| @@ -136,7 +139,7 @@ impl ImageOp { | |||
| hasher.write_u8(quality); | |||
| let hash = hasher.finish(); | |||
| ImageOp { source, op, quality, hash, collision: None } | |||
| ImageOp { source, op, quality, hash, collision_id: 0 } | |||
| } | |||
| pub fn from_args(source: String, op: &str, width: Option<u32>, height: Option<u32>, quality: u8) -> Result<ImageOp> { | |||
| @@ -144,8 +147,6 @@ impl ImageOp { | |||
| Ok(Self::new(source, op, quality)) | |||
| } | |||
| fn num_collisions(&self) -> u32 { self.collision.unwrap_or(0) } | |||
| fn perform(&self, content_path: &Path, target_path: &Path) -> Result<()> { | |||
| use ResizeOp::*; | |||
| @@ -263,40 +264,44 @@ impl Processor { | |||
| // This is detected when there is an ImageOp with the same hash in the `img_ops` map but which is not equal to this one. | |||
| // To deal with this, all collisions get a (random) sequential ID number. | |||
| // First try to look up this ImageOp in `img_ops_collisions`, maybe we've already seen the same ImageOp | |||
| let mut num = 1; | |||
| // First try to look up this ImageOp in `img_ops_collisions`, maybe we've already seen the same ImageOp. | |||
| // At the same time, count IDs to figure out the next free one. | |||
| // Start with the ID of 2, because we'll need to use 1 for the ImageOp already present in the map: | |||
| let mut collision_id = 2; | |||
| for op in self.img_ops_collisions.iter().filter(|op| op.hash == img_op.hash) { | |||
| if *op == img_op { | |||
| // This is a colliding ImageOp, but we've already seen the very same one, so just return its ID | |||
| return num; | |||
| // This is a colliding ImageOp, but we've already seen an equal one (not just by hash, but by content too), | |||
| // so just return its ID: | |||
| return collision_id; | |||
| } else { | |||
| num += 1; | |||
| collision_id += 1; | |||
| } | |||
| } | |||
| // If we get here, that means this is a new colliding ImageOp and `num` has the next free ID | |||
| if num == 1 { | |||
| self.img_ops.get_mut(&img_op.hash).unwrap().collision = Some(0); | |||
| // If we get here, that means this is a new colliding ImageOp and `collision_id` is the next free ID | |||
| if collision_id == 2 { | |||
| // This is the first collision found with this hash, update the ID of the matching ImageOp in the map. | |||
| self.img_ops.get_mut(&img_op.hash).unwrap().collision_id = 1; | |||
| } | |||
| img_op.collision = Some(num); | |||
| img_op.collision_id = collision_id; | |||
| self.img_ops_collisions.push(img_op); | |||
| num | |||
| collision_id | |||
| } | |||
| fn op_filename(hash: u64, num_collisions: u32) -> String { | |||
| fn op_filename(hash: u64, collision_id: u32) -> String { | |||
| // Please keep this in sync with RESIZED_FILENAME | |||
| assert!(num_collisions < 256, "Unexpectedly large number of collisions: {}", num_collisions); | |||
| format!("{:016x}{:02x}.jpg", hash, num_collisions) | |||
| assert!(collision_id < 256, "Unexpectedly large number of collisions: {}", collision_id); | |||
| format!("{:016x}{:02x}.jpg", hash, collision_id) | |||
| } | |||
| fn op_url(&self, hash: u64, num_collisions: u32) -> String { | |||
| format!("{}/{}", &self.resized_url, Self::op_filename(hash, num_collisions)) | |||
| fn op_url(&self, hash: u64, collision_id: u32) -> String { | |||
| format!("{}/{}", &self.resized_url, Self::op_filename(hash, collision_id)) | |||
| } | |||
| pub fn insert(&mut self, img_op: ImageOp) -> String { | |||
| let hash = img_op.hash; | |||
| let num_collisions = self.insert_with_collisions(img_op); | |||
| self.op_url(hash, num_collisions) | |||
| let collision_id = self.insert_with_collisions(img_op); | |||
| self.op_url(hash, collision_id) | |||
| } | |||
| pub fn prune(&self) -> Result<()> { | |||
| @@ -308,8 +313,8 @@ impl Processor { | |||
| let filename = entry_path.file_name().unwrap().to_string_lossy(); | |||
| if let Some(capts) = RESIZED_FILENAME.captures(filename.as_ref()) { | |||
| let hash = u64::from_str_radix(capts.get(1).unwrap().as_str(), 16).unwrap(); | |||
| let num_collisions = u32::from_str_radix(capts.get(2).unwrap().as_str(), 16).unwrap(); | |||
| if num_collisions > 0 || !self.img_ops.contains_key(&hash) { | |||
| let collision_id = u32::from_str_radix(capts.get(2).unwrap().as_str(), 16).unwrap(); | |||
| if collision_id > 0 || !self.img_ops.contains_key(&hash) { | |||
| fs::remove_file(&entry_path)?; | |||
| } | |||
| } | |||
| @@ -320,7 +325,7 @@ impl Processor { | |||
| pub fn do_process(&mut self) -> Result<()> { | |||
| self.img_ops.par_iter().map(|(hash, op)| { | |||
| let target = self.resized_path.join(Self::op_filename(*hash, op.num_collisions())); | |||
| let target = self.resized_path.join(Self::op_filename(*hash, op.collision_id)); | |||
| op.perform(&self.content_path, &target) | |||
| .chain_err(|| format!("Failed to process image: {}", op.source)) | |||
| }) | |||