diff --git a/src/decoder/ifd.rs b/src/decoder/ifd.rs index b349e64..930f128 100644 --- a/src/decoder/ifd.rs +++ b/src/decoder/ifd.rs @@ -4,9 +4,9 @@ use std::io::{self, Read, Seek}; use std::collections::{HashMap}; use super::stream::{ByteOrder, SmartReader, EndianReader}; -use ::{TiffError, TiffResult}; +use ::{TiffError, TiffFormatError, TiffUnsupportedError, TiffResult}; -use self::Value::{Unsigned, List, Rational}; +use self::Value::{Unsigned, List, Rational, Ascii}; macro_rules! tags { {$( @@ -33,7 +33,7 @@ macro_rules! tags { // Note: These tags appear in the order they are mentioned in the TIFF reference tags!{ // Baseline tags: - Artist 315; // TODO add support + Artist 315; // grayscale images PhotometricInterpretation 1 or 3 BitsPerSample 258; CellLength 265; // TODO add support @@ -41,22 +41,22 @@ tags!{ // palette-color images (PhotometricInterpretation 3) ColorMap 320; // TODO add support Compression 259; // TODO add support for 2 and 32773 - Copyright 33_432; // TODO add support - DateTime 306; // TODO add support + Copyright 33_432; + DateTime 306; ExtraSamples 338; // TODO add support FillOrder 266; // TODO add support FreeByteCounts 289; // TODO add support FreeOffsets 288; // TODO add support GrayResponseCurve 291; // TODO add support GrayResponseUnit 290; // TODO add support - HostComputer 316; // TODO add support - ImageDescription 270; // TODO add support + HostComputer 316; + ImageDescription 270; ImageLength 257; ImageWidth 256; - Make 271; // TODO add support + Make 271; MaxSampleValue 281; // TODO add support MinSampleValue 280; // TODO add support - Model 272; // TODO add support + Model 272; NewSubfileType 254; // TODO add support Orientation 274; // TODO add support PhotometricInterpretation 262; @@ -86,21 +86,20 @@ pub enum Type { #[allow(unused_qualifications)] -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Value { //Signed(i32), Unsigned(u32), List(Vec), - Rational(u32, u32) + Rational(u32, u32), + Ascii(String) } impl Value { pub fn into_u32(self) -> TiffResult { match self { Unsigned(val) => Ok(val), - val => Err(TiffError::FormatError(format!( - "Expected unsigned integer, {:?} found.", val - ))) + val => Err(TiffError::FormatError(TiffFormatError::UnsignedIntegerExpected(val))), } } pub fn into_u32_vec(self) -> TiffResult> { @@ -113,8 +112,8 @@ impl Value { Ok(new_vec) }, Unsigned(val) => Ok(vec![val]), - Rational(numerator, denominator) => Ok(vec![numerator, denominator]) - //_ => Err(::image::FormatError("Tag data malformed.".to_string())) + Rational(numerator, denominator) => Ok(vec![numerator, denominator]), + Ascii(val) => Ok(val.chars().map(|x| x as u32).collect()) } } } @@ -196,7 +195,12 @@ impl Entry { } Ok(List(v)) }, - _ => Err(TiffError::UnsupportedError("Unsupported data type.".to_string())) + (Type::ASCII, n) => { + try!(decoder.goto_offset(try!(self.r(bo).read_u32()))); + let string = try!(decoder.read_string(n as usize)); + Ok(Ascii(string)) + } + _ => Err(TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedDataType)) } } } diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 48580b4..9dc07c6 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -2,8 +2,9 @@ use std::io::{self, Read, Seek}; use std::mem; use num_traits::{FromPrimitive, Num}; use std::collections::HashMap; +use std::string::FromUtf8Error; -use ::{ColorType, TiffError, TiffResult}; +use ::{ColorType, TiffError, TiffFormatError, TiffUnsupportedError, TiffResult}; use self::ifd::Directory; @@ -35,8 +36,8 @@ enum DecodingBuffer<'a> { U16(&'a mut [u16]) } -#[derive(Clone, Copy, Debug, PartialEq, FromPrimitive)] -enum PhotometricInterpretation { +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, FromPrimitive)] +pub enum PhotometricInterpretation { WhiteIsZero = 0, BlackIsZero = 1, RGB = 2, @@ -47,8 +48,8 @@ enum PhotometricInterpretation { CIELab = 8, } -#[derive(Clone, Copy, Debug, FromPrimitive)] -enum CompressionMethod { +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, FromPrimitive)] +pub enum CompressionMethod { None = 1, Huffman = 2, Fax3 = 3, @@ -58,13 +59,13 @@ enum CompressionMethod { PackBits = 0x8005 } -#[derive(Clone, Copy, Debug, FromPrimitive)] -enum PlanarConfiguration { +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, FromPrimitive)] +pub enum PlanarConfiguration { Chunky = 1, Planar = 2 } -#[derive(Clone, Copy, Debug, FromPrimitive)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, FromPrimitive)] enum Predictor { None = 1, Horizontal = 2 @@ -125,9 +126,7 @@ fn rev_hpredict(image: DecodingResult, size: (u32, u32), color_type: ColorType) ColorType::Gray(8) | ColorType::Gray(16) => 1, ColorType::RGB(8) | ColorType::RGB(16) => 3, ColorType::RGBA(8) | ColorType::RGBA(16) | ColorType::CMYK(8) => 4, - _ => return Err(TiffError::UnsupportedError(format!( - "Horizontal predictor for {:?} is unsupported.", color_type - ))) + _ => return Err(TiffError::UnsupportedError(TiffUnsupportedError::HorizontalPredictor(color_type))) }; Ok(match image { DecodingResult::U8(buf) => { @@ -171,9 +170,8 @@ impl Decoder { PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero if self.bits_per_sample.len() == 1 => Ok(ColorType::Gray(self.bits_per_sample[0])), - _ => Err(TiffError::UnsupportedError(format!( - "{:?} with {:?} bits per sample is unsupported", self.photometric_interpretation, self.bits_per_sample - ))) // TODO: this is bad we should not fail at this point} + // TODO: this is bad we should not fail at this point + _ => Err(TiffError::UnsupportedError(TiffUnsupportedError::InterpretationWithBits(self.photometric_interpretation, self.bits_per_sample.clone()))) } } @@ -187,12 +185,10 @@ impl Decoder { b"MM" => { self.byte_order = ByteOrder::BigEndian; self.reader.byte_order = ByteOrder::BigEndian; }, - _ => return Err(TiffError::FormatError( - "TIFF signature not found.".to_string() - )) + _ => return Err(TiffError::FormatError(TiffFormatError::TiffSignatureNotFound)) } if try!(self.read_short()) != 42 { - return Err(TiffError::FormatError("TIFF signature invalid.".to_string())) + return Err(TiffError::FormatError(TiffFormatError::TiffSignatureInvalid)) } self.next_ifd = match try!(self.read_long()) { 0 => None, @@ -218,18 +214,14 @@ impl Decoder { try!(self.get_tag_u32(ifd::Tag::PhotometricInterpretation)) ) { Some(val) => val, - None => return Err(TiffError::UnsupportedError( - "The image is using an unknown photometric interpretation.".to_string() - )) + None => return Err(TiffError::UnsupportedError(TiffUnsupportedError::UnknownInterpretation)) }; if let Some(val) = try!(self.find_tag_u32(ifd::Tag::Compression)) { match FromPrimitive::from_u32(val) { Some(method) => { self.compression_method = method }, - None => return Err(TiffError::UnsupportedError( - "Unknown compression method.".to_string() - )) + None => return Err(TiffError::UnsupportedError(TiffUnsupportedError::UnknownCompressionMethod)) } } if let Some(val) = try!(self.find_tag_u32(ifd::Tag::SamplesPerPixel)) { @@ -247,9 +239,7 @@ impl Decoder { } } - _ => return Err(TiffError::UnsupportedError( - format!("{} samples per pixel is supported.", self.samples) - )) + _ => return Err(TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedSampleDepth(self.samples))) } Ok(self) } @@ -279,6 +269,16 @@ impl Decoder { self.reader.read_u32() } + /// Reads a string + #[inline] + pub fn read_string(&mut self, length: usize) -> Result { + let mut out = String::with_capacity(length); + self.reader.read_to_string(&mut out); + // Strings may be null-terminated, so we trim anything downstream of the null byte + let trimmed = out.bytes().take_while(|&n| n != 0).collect::>(); + String::from_utf8(trimmed) + } + /// Reads a TIFF IFA offset/value field #[inline] pub fn read_offset(&mut self) -> Result<[u8; 4], io::Error> { @@ -323,9 +323,7 @@ impl Decoder { fn read_ifd(&mut self) -> TiffResult { let mut dir: Directory = HashMap::new(); match self.next_ifd { - None => return Err(TiffError::FormatError( - "Image file directory not found.".to_string()) - ), + None => return Err(TiffError::FormatError(TiffFormatError::ImageFileDirectoryNotFound)), Some(offset) => try!(self.goto_offset(offset)) } for _ in 0..try!(self.read_short()) { @@ -379,9 +377,7 @@ impl Decoder { pub fn get_tag(&mut self, tag: ifd::Tag) -> TiffResult { match try!(self.find_tag(tag)) { Some(val) => Ok(val), - None => Err(TiffError::FormatError(format!( - "Required tag `{:?}` not found.", tag - ))) + None => Err(TiffError::FormatError(TiffFormatError::RequiredTagNotFound(tag))), } } @@ -414,13 +410,11 @@ impl Decoder { let (bytes, reader) = try!(PackBitsReader::new(&mut self.reader, order, length as usize)); (bytes, Box::new(reader)) }, - method => return Err(TiffError::UnsupportedError(format!( - "Compression method {:?} is unsupported", method - ))) + method => return Err(TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedCompressionMethod(method))) }; Ok(match (color_type, buffer) { (ColorType:: RGB(8), DecodingBuffer::U8(ref mut buffer)) | - (ColorType::RGBA(8), DecodingBuffer::U8(ref mut buffer)) | + (ColorType::RGBA(8), DecodingBuffer::U8(ref mut buffer)) | (ColorType::CMYK(8), DecodingBuffer::U8(ref mut buffer)) => { try!(reader.read_exact(&mut buffer[..bytes])); bytes @@ -448,9 +442,7 @@ impl Decoder { } bytes } - (type_, _) => return Err(TiffError::UnsupportedError(format!( - "Color type {:?} is unsupported", type_ - ))) + (type_, _) => return Err(TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedColorType(type_))), }) } @@ -468,18 +460,12 @@ impl Decoder { let mut result = match self.bits_per_sample.iter().cloned().max().unwrap_or(8) { n if n <= 8 => DecodingResult::U8(vec![0; buffer_size]), n if n <= 16 => DecodingResult::U16(vec![0; buffer_size]), - n => return Err( - TiffError::UnsupportedError( - format!("{} bits per channel not supported", n) - ) - ) + n => return Err(TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedBitsPerChannel(n))), }; if let Ok(config) = self.get_tag_u32(ifd::Tag::PlanarConfiguration) { match FromPrimitive::from_u32(config) { Some(PlanarConfiguration::Chunky) => {}, - config => return Err(TiffError::UnsupportedError( - format!("Unsupported planar configuration “{:?}”.", config) - )) + config => return Err(TiffError::UnsupportedError(TiffUnsupportedError::UnsupportedPlanarConfig(config))) } } let mut units_read = 0; @@ -519,9 +505,7 @@ impl Decoder { try!(self.colortype()) )) }, - None => return Err(TiffError::FormatError( - format!("Unknown predictor “{}” encountered", predictor) - )) + None => return Err(TiffError::FormatError(TiffFormatError::UnknownPredictor(predictor))), } } Ok(result) diff --git a/src/error.rs b/src/error.rs index bc6bf70..113d793 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,20 +1,83 @@ use std::error::Error; use std::fmt; use std::io; +use std::string; + +use decoder::ifd::{Tag, Value}; +use decoder::{PhotometricInterpretation, CompressionMethod, PlanarConfiguration}; +use ColorType; /// Tiff error kinds. #[derive(Debug)] pub enum TiffError { /// The Image is not formatted properly - FormatError(String), + FormatError(TiffFormatError), /// The Decoder does not support this image format - UnsupportedError(String), + UnsupportedError(TiffUnsupportedError), /// An I/O Error occurred while decoding the image IoError(io::Error), } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TiffFormatError { + TiffSignatureNotFound, + TiffSignatureInvalid, + ImageFileDirectoryNotFound, + RequiredTagNotFound(Tag), + UnknownPredictor(u32), + UnsignedIntegerExpected(Value), +} + +impl fmt::Display for TiffFormatError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use self::TiffFormatError::*; + match *self { + TiffSignatureNotFound => write!(fmt, "TIFF signature not found."), + TiffSignatureInvalid => write!(fmt, "TIFF signature invalid."), + ImageFileDirectoryNotFound => write!(fmt, "Image file directory not found."), + RequiredTagNotFound(ref tag) => write!(fmt, "Required tag `{:?}` not found.", tag), + UnknownPredictor(ref predictor) => write!(fmt, "Unknown predictor “{}” encountered", predictor), + UnsignedIntegerExpected(ref val) => write!(fmt, "Expected unsigned integer, {:?} found.", val), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TiffUnsupportedError { + HorizontalPredictor(ColorType), + InterpretationWithBits(PhotometricInterpretation, Vec), + UnknownInterpretation, + UnknownCompressionMethod, + UnsupportedCompressionMethod(CompressionMethod), + UnsupportedSampleDepth(u8), + UnsupportedColorType(ColorType), + UnsupportedBitsPerChannel(u8), + UnsupportedPlanarConfig(Option), + UnsupportedDataType, +} + +impl fmt::Display for TiffUnsupportedError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use self::TiffUnsupportedError::*; + match *self { + HorizontalPredictor(color_type) => write!(fmt, "Horizontal predictor for {:?} is unsupported.", color_type), + InterpretationWithBits(ref photometric_interpretation, ref bits_per_sample) => { + write!(fmt, "{:?} with {:?} bits per sample is unsupported", photometric_interpretation, bits_per_sample) + }, + UnknownInterpretation => write!(fmt, "The image is using an unknown photometric interpretation."), + UnknownCompressionMethod => write!(fmt, "Unknown compression method."), + UnsupportedCompressionMethod(method) => write!(fmt, "Compression method {:?} is unsupported", method), + UnsupportedSampleDepth(samples) => write!(fmt, "{} samples per pixel is supported.", samples), + UnsupportedColorType(color_type) => write!(fmt, "Color type {:?} is unsupported", color_type), + UnsupportedBitsPerChannel(bits) => write!(fmt, "{} bits per channel not supported", bits), + UnsupportedPlanarConfig(config) => write!(fmt, "Unsupported planar configuration “{:?}”.", config), + UnsupportedDataType => write!(fmt, "Unsupported data type."), + } + } +} + impl fmt::Display for TiffError { fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { @@ -49,5 +112,11 @@ impl From for TiffError { } } +impl From for TiffError { + fn from(err: string::FromUtf8Error) -> TiffError { + TiffError::FormatError(String::from("Image contains invalid tag.")) + } +} + /// Result of an image decoding/encoding process pub type TiffResult = Result; diff --git a/src/lib.rs b/src/lib.rs index f95658c..2722a90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ extern crate num_traits; pub mod decoder; mod error; -pub use self::error::{TiffError, TiffResult}; +pub use self::error::{TiffError, TiffFormatError, TiffUnsupportedError, TiffResult}; /// An enumeration over supported color types and their bit depths #[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] diff --git a/tests/decode_images.rs b/tests/decode_images.rs index f8e3a25..5881733 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -1,7 +1,7 @@ extern crate tiff; use tiff::ColorType; -use tiff::decoder::{Decoder, DecodingResult}; +use tiff::decoder::{Decoder, DecodingResult, ifd::Tag, ifd::Value}; use std::fs::File; @@ -45,6 +45,24 @@ fn test_rgb_u16() assert!(img_res.is_ok()); } +#[test] +fn test_string_tags() +{ + // these files have null-terminated strings for their Software tag. One has extra bytes after + // the null byte, so we check both to ensure that we're truncating properly + let filenames = vec!["minisblack-1c-16b.tiff", "rgb-3c-16b.tiff"]; + for filename in filenames.iter() { + let path = format!("./tests/images/{}", filename); + let img_file = File::open(path).expect("can't open file"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + let software= decoder.get_tag(Tag::Software).unwrap(); + match software { + Value::Ascii(s) => assert_eq!(s, String::from("GraphicsMagick 1.2 unreleased Q16 http://www.GraphicsMagick.org/")), + _ => assert!(false) + }; + } +} + // TODO: GrayA support //#[test] //fn test_gray_alpha_u8()