From d5f0ac17a7d28dda1601afea58a03c88fc84862f Mon Sep 17 00:00:00 2001 From: ophir Date: Sat, 23 Nov 2019 21:20:13 +0100 Subject: [PATCH 1/2] Allow setting a custom pixel density in the JPEG encoder The encoder used to hardcode a pixel aspect ratio of 1x1 It allows setting a custom DPI value. Fixes #1077 --- src/jpeg/encoder.rs | 96 ++++++++++++++++++++++++++++++++++++++++++--- src/jpeg/mod.rs | 2 +- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/jpeg/encoder.rs b/src/jpeg/encoder.rs index 907924e3d3..7ce986d0ea 100644 --- a/src/jpeg/encoder.rs +++ b/src/jpeg/encoder.rs @@ -274,6 +274,59 @@ impl<'a, W: Write + 'a> BitWriter<'a, W> { } } +/// Represents a unit in which the density of an image is measured +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PixelDensityUnit { + /// Represents the absence of a unit, the values indicate only a + /// [pixel aspect ratio](https://en.wikipedia.org/wiki/Pixel_aspect_ratio) + PixelAspectRatio, + + /// Pixels per inch (2.54 cm) + Inches, + + /// Pixels per centimeter + Centimeters, +} + +/// Represents the pixel density of an image +/// +/// For example, a 300 DPI image is represented by: +/// +/// ```rust +/// use image::jpeg::*; +/// let hdpi = PixelDensity::dpi(300); +/// assert_eq!(hdpi, PixelDensity {density: (300,300), unit: PixelDensityUnit::Inches}) +/// ``` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PixelDensity { + /// A couple of values for (Xdensity, Ydensity) + pub density: (u16, u16), + /// The unit in which the density is measured + pub unit: PixelDensityUnit, +} + +impl PixelDensity { + /// Creates the most common pixel density type: + /// the horizontal and the vertical density are equal, + /// and measured in pixels per inch. + pub fn dpi(density: u16) -> Self { + PixelDensity { + density: (density, density), + unit: PixelDensityUnit::Inches, + } + } +} + +impl Default for PixelDensity { + /// Returns a pixel density with a pixel aspect ratio of 1 + fn default() -> Self { + PixelDensity { + density: (1, 1), + unit: PixelDensityUnit::PixelAspectRatio, + } + } +} + /// The representation of a JPEG encoder pub struct JPEGEncoder<'a, W: 'a> { writer: BitWriter<'a, W>, @@ -285,6 +338,8 @@ pub struct JPEGEncoder<'a, W: 'a> { luma_actable: Vec<(u8, u16)>, chroma_dctable: Vec<(u8, u16)>, chroma_actable: Vec<(u8, u16)>, + + pixel_density: PixelDensity, } impl<'a, W: Write> JPEGEncoder<'a, W> { @@ -360,9 +415,19 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { luma_actable: la, chroma_dctable: cd, chroma_actable: ca, + + pixel_density: PixelDensity::default(), } } + /// Set the pixel density of the images the encoder will encode. + /// If this method is not called, then a default pixel aspect ratio of 1x1 will be applied, + /// and no DPI information will be stored in the image. + pub fn set_pixel_density(&mut self, pixel_density: PixelDensity) -> &mut Self { + self.pixel_density = pixel_density; + self + } + /// Encodes the image ```image``` /// that has dimensions ```width``` and ```height``` /// and ```ColorType``` ```c``` @@ -382,7 +447,7 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { let mut buf = Vec::new(); - build_jfif_header(&mut buf); + build_jfif_header(&mut buf, self.pixel_density); self.writer.write_segment(APP0, Some(&buf))?; build_frame_header( @@ -567,16 +632,20 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { } } -fn build_jfif_header(m: &mut Vec) { +fn build_jfif_header(m: &mut Vec, density: PixelDensity) { m.clear(); let _ = write!(m, "JFIF"); let _ = m.write_all(&[0]); let _ = m.write_all(&[0x01]); let _ = m.write_all(&[0x02]); - let _ = m.write_all(&[0]); - let _ = m.write_u16::(1); - let _ = m.write_u16::(1); + let _ = m.write_all(&[match density.unit { + PixelDensityUnit::PixelAspectRatio => 0x00, + PixelDensityUnit::Inches => 0x01, + PixelDensityUnit::Centimeters => 0x02, + }]); + let _ = m.write_u16::(density.density.0); + let _ = m.write_u16::(density.density.1); let _ = m.write_all(&[0]); let _ = m.write_all(&[0]); } @@ -752,7 +821,7 @@ fn copy_blocks_gray( #[cfg(test)] mod tests { use super::super::JpegDecoder; - use super::JPEGEncoder; + use super::{JPEGEncoder, PixelDensity, build_jfif_header}; use color::ColorType; use image::ImageDecoder; use std::io::Cursor; @@ -818,4 +887,19 @@ mod tests { assert!(decoded[3] > 0x80); } } + + #[test] + fn jfif_header_density_check() { + let mut buffer = Vec::new(); + build_jfif_header(&mut buffer, PixelDensity::dpi(300)); + assert_eq!(buffer, vec![ + b'J', b'F', b'I', b'F', + 0, 1, 2, // JFIF version 1.2 + 1, // density is in dpi + 300u16.to_be_bytes()[0], 300u16.to_be_bytes()[1], + 300u16.to_be_bytes()[0], 300u16.to_be_bytes()[1], + 0, 0, // No thumbnail + ] + ); + } } diff --git a/src/jpeg/mod.rs b/src/jpeg/mod.rs index 2724934ee4..4d9bf7a8d6 100644 --- a/src/jpeg/mod.rs +++ b/src/jpeg/mod.rs @@ -8,7 +8,7 @@ //! pub use self::decoder::JpegDecoder; -pub use self::encoder::JPEGEncoder; +pub use self::encoder::{JPEGEncoder, PixelDensity, PixelDensityUnit}; mod decoder; mod encoder; From 76abfcf508865a2fbedc42b5cdb89545c792b1b2 Mon Sep 17 00:00:00 2001 From: ophir Date: Sat, 23 Nov 2019 23:32:44 +0100 Subject: [PATCH 2/2] Remove the ability to chain method calls in JpegDecoder::set_pixel_density requested by @fintelia in https://github.com/image-rs/image/pull/1078#pullrequestreview-321931042 --- src/jpeg/encoder.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/jpeg/encoder.rs b/src/jpeg/encoder.rs index 7ce986d0ea..97ddfb4a34 100644 --- a/src/jpeg/encoder.rs +++ b/src/jpeg/encoder.rs @@ -423,9 +423,8 @@ impl<'a, W: Write> JPEGEncoder<'a, W> { /// Set the pixel density of the images the encoder will encode. /// If this method is not called, then a default pixel aspect ratio of 1x1 will be applied, /// and no DPI information will be stored in the image. - pub fn set_pixel_density(&mut self, pixel_density: PixelDensity) -> &mut Self { + pub fn set_pixel_density(&mut self, pixel_density: PixelDensity) { self.pixel_density = pixel_density; - self } /// Encodes the image ```image```