Import tiff module from image repo
This commit is contained in:
1 parent
796a8d0538
commit
a92205909a
6 files changed
+976
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
/target/
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "image-tiff"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["R.Chavignat <r.chavignat@gmail.com>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
byteorder = "1.2.1"
|
||||||
|
lzw = "0.10.0"
|
||||||
|
num-traits = "0.1.32"
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
//! Function for reading TIFF tags
|
||||||
|
|
||||||
|
use std::io::{self, Read, Seek};
|
||||||
|
use std::collections::{HashMap};
|
||||||
|
|
||||||
|
use super::stream::{ByteOrder, SmartReader, EndianReader};
|
||||||
|
|
||||||
|
use self::Value::{Unsigned, List};
|
||||||
|
|
||||||
|
macro_rules! tags {
|
||||||
|
{$(
|
||||||
|
$tag:ident
|
||||||
|
$val:expr;
|
||||||
|
)*} => {
|
||||||
|
|
||||||
|
/// TIFF tag
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||||
|
pub enum Tag {
|
||||||
|
$($tag,)*
|
||||||
|
Unknown(u16)
|
||||||
|
}
|
||||||
|
impl Tag {
|
||||||
|
pub fn from_u16(n: u16) -> Tag {
|
||||||
|
$(if n == $val { Tag::$tag } else)* {
|
||||||
|
Tag::Unknown(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: These tags appear in the order they are mentioned in the TIFF reference
|
||||||
|
tags!{
|
||||||
|
// Baseline tags:
|
||||||
|
Artist 315; // TODO add support
|
||||||
|
// grayscale images PhotometricInterpretation 1 or 3
|
||||||
|
BitsPerSample 258;
|
||||||
|
CellLength 265; // TODO add support
|
||||||
|
CellWidth 264; // TODO add support
|
||||||
|
// palette-color images (PhotometricInterpretation 3)
|
||||||
|
ColorMap 320; // TODO add support
|
||||||
|
Compression 259; // TODO add support for 2 and 32773
|
||||||
|
Copyright 33432; // TODO add support
|
||||||
|
DateTime 306; // TODO add support
|
||||||
|
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
|
||||||
|
ImageLength 257;
|
||||||
|
ImageWidth 256;
|
||||||
|
Make 271; // TODO add support
|
||||||
|
MaxSampleValue 281; // TODO add support
|
||||||
|
MinSampleValue 280; // TODO add support
|
||||||
|
Model 272; // TODO add support
|
||||||
|
NewSubfileType 254; // TODO add support
|
||||||
|
Orientation 274; // TODO add support
|
||||||
|
PhotometricInterpretation 262;
|
||||||
|
PlanarConfiguration 284;
|
||||||
|
ResolutionUnit 296; // TODO add support
|
||||||
|
RowsPerStrip 278;
|
||||||
|
SamplesPerPixel 277;
|
||||||
|
Software 305;
|
||||||
|
StripByteCounts 279;
|
||||||
|
StripOffsets 273;
|
||||||
|
SubfileType 255; // TODO add support
|
||||||
|
Threshholding 263; // TODO add support
|
||||||
|
XResolution 282;
|
||||||
|
YResolution 283;
|
||||||
|
// Advanced tags
|
||||||
|
Predictor 317;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum_from_primitive! {
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Type {
|
||||||
|
BYTE = 1,
|
||||||
|
ASCII = 2,
|
||||||
|
SHORT = 3,
|
||||||
|
LONG = 4,
|
||||||
|
RATIONAL = 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(unused_qualifications)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Value {
|
||||||
|
//Signed(i32),
|
||||||
|
Unsigned(u32),
|
||||||
|
List(Vec<Value>)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
pub fn into_u32(self) -> ::image::ImageResult<u32> {
|
||||||
|
match self {
|
||||||
|
Unsigned(val) => Ok(val),
|
||||||
|
val => Err(::image::ImageError::FormatError(format!(
|
||||||
|
"Expected unsigned integer, {:?} found.", val
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn into_u32_vec(self) -> ::image::ImageResult<Vec<u32>> {
|
||||||
|
match self {
|
||||||
|
List(vec) => {
|
||||||
|
let mut new_vec = Vec::with_capacity(vec.len());
|
||||||
|
for v in vec {
|
||||||
|
new_vec.push(v.into_u32()?)
|
||||||
|
}
|
||||||
|
Ok(new_vec)
|
||||||
|
},
|
||||||
|
Unsigned(val) => Ok(vec![val]),
|
||||||
|
//_ => Err(::image::FormatError("Tag data malformed.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Entry {
|
||||||
|
type_: Type,
|
||||||
|
count: u32,
|
||||||
|
offset: [u8; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::fmt::Debug for Entry {
|
||||||
|
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
||||||
|
fmt.write_str(&format!("Entry {{ type_: {:?}, count: {:?}, offset: {:?} }}",
|
||||||
|
self.type_,
|
||||||
|
self.count,
|
||||||
|
&self.offset
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
pub fn new(type_: Type, count: u32, offset: [u8; 4]) -> Entry {
|
||||||
|
Entry {
|
||||||
|
type_: type_,
|
||||||
|
count: count,
|
||||||
|
offset: offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mem_reader for the offset/value field
|
||||||
|
fn r(&self, byte_order: ByteOrder) -> SmartReader<io::Cursor<Vec<u8>>> {
|
||||||
|
SmartReader::wrap(
|
||||||
|
io::Cursor::new(self.offset.to_vec()),
|
||||||
|
byte_order
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn val<R: Read + Seek>(&self, decoder: &mut super::TIFFDecoder<R>)
|
||||||
|
-> ::image::ImageResult<Value> {
|
||||||
|
let bo = decoder.byte_order();
|
||||||
|
match (self.type_, self.count) {
|
||||||
|
// TODO check if this could give wrong results
|
||||||
|
// at a different endianess of file/computer.
|
||||||
|
(Type::BYTE, 1) => Ok(Unsigned(u32::from(self.offset[0]))),
|
||||||
|
(Type::SHORT, 1) => Ok(Unsigned(u32::from(self.r(bo).read_u16()?))),
|
||||||
|
(Type::SHORT, 2) => {
|
||||||
|
let mut r = self.r(bo);
|
||||||
|
Ok(List(vec![
|
||||||
|
Unsigned(u32::from(r.read_u16()?)),
|
||||||
|
Unsigned(u32::from(r.read_u16()?))
|
||||||
|
]))
|
||||||
|
},
|
||||||
|
(Type::SHORT, n) => {
|
||||||
|
let mut v = Vec::with_capacity(n as usize);
|
||||||
|
try!(decoder.goto_offset(try!(self.r(bo).read_u32())));
|
||||||
|
for _ in 0 .. n {
|
||||||
|
v.push(Unsigned(u32::from(decoder.read_short()?)))
|
||||||
|
}
|
||||||
|
Ok(List(v))
|
||||||
|
},
|
||||||
|
(Type::LONG, 1) => Ok(Unsigned(try!(self.r(bo).read_u32()))),
|
||||||
|
(Type::LONG, n) => {
|
||||||
|
let mut v = Vec::with_capacity(n as usize);
|
||||||
|
try!(decoder.goto_offset(try!(self.r(bo).read_u32())));
|
||||||
|
for _ in 0 .. n {
|
||||||
|
v.push(Unsigned(try!(decoder.read_long())))
|
||||||
|
}
|
||||||
|
Ok(List(v))
|
||||||
|
}
|
||||||
|
_ => Err(::image::ImageError::UnsupportedError("Unsupported data type.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type representing an Image File Directory
|
||||||
|
pub type Directory = HashMap<Tag, Entry>;
|
||||||
@@ -0,0 +1,551 @@
|
|||||||
|
use std::io::{self, Read, Seek};
|
||||||
|
use std::mem;
|
||||||
|
use num_traits::{FromPrimitive, Num};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use image;
|
||||||
|
use image::{
|
||||||
|
ImageError,
|
||||||
|
ImageResult,
|
||||||
|
ImageDecoder,
|
||||||
|
DecodingResult,
|
||||||
|
DecodingBuffer
|
||||||
|
};
|
||||||
|
|
||||||
|
use color::{ColorType};
|
||||||
|
|
||||||
|
use self::ifd::Directory;
|
||||||
|
|
||||||
|
use self::stream::{
|
||||||
|
ByteOrder,
|
||||||
|
EndianReader,
|
||||||
|
SmartReader,
|
||||||
|
LZWReader,
|
||||||
|
PackBitsReader
|
||||||
|
};
|
||||||
|
|
||||||
|
mod ifd;
|
||||||
|
mod stream;
|
||||||
|
|
||||||
|
enum_from_primitive! {
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum PhotometricInterpretation {
|
||||||
|
WhiteIsZero = 0,
|
||||||
|
BlackIsZero = 1,
|
||||||
|
RGB = 2,
|
||||||
|
RGBPalette = 3,
|
||||||
|
TransparencyMask = 4,
|
||||||
|
CMYK = 5,
|
||||||
|
YCbCr = 6,
|
||||||
|
CIELab = 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum_from_primitive! {
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum CompressionMethod {
|
||||||
|
None = 1,
|
||||||
|
Huffman = 2,
|
||||||
|
Fax3 = 3,
|
||||||
|
Fax4 = 4,
|
||||||
|
LZW = 5,
|
||||||
|
JPEG = 6,
|
||||||
|
PackBits = 0x8005
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum_from_primitive! {
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum PlanarConfiguration {
|
||||||
|
Chunky = 1,
|
||||||
|
Planar = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum_from_primitive! {
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum Predictor {
|
||||||
|
None = 1,
|
||||||
|
Horizontal = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The representation of a TIFF decoder
|
||||||
|
///
|
||||||
|
/// Currently does not support decoding of interlaced images
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TIFFDecoder<R> where R: Read + Seek {
|
||||||
|
reader: SmartReader<R>,
|
||||||
|
byte_order: ByteOrder,
|
||||||
|
next_ifd: Option<u32>,
|
||||||
|
ifd: Option<Directory>,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
bits_per_sample: Vec<u8>,
|
||||||
|
samples: u8,
|
||||||
|
photometric_interpretation: PhotometricInterpretation,
|
||||||
|
compression_method: CompressionMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Wrapping {
|
||||||
|
fn wrapping_add(&self, other: Self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wrapping for u8 {
|
||||||
|
fn wrapping_add(&self, other: Self) -> Self {
|
||||||
|
u8::wrapping_add(*self, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wrapping for u16 {
|
||||||
|
fn wrapping_add(&self, other: Self) -> Self {
|
||||||
|
u16::wrapping_add(*self, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rev_hpredict_nsamp<T>(mut image: Vec<T>,
|
||||||
|
size: (u32, u32),
|
||||||
|
samples: usize)
|
||||||
|
-> Vec<T>
|
||||||
|
where T: Num + Copy + Wrapping {
|
||||||
|
let width = size.0 as usize;
|
||||||
|
let height = size.1 as usize;
|
||||||
|
for row in 0..height {
|
||||||
|
for col in samples..width * samples {
|
||||||
|
let prev_pixel = image[(row * width * samples + col - samples)];
|
||||||
|
let pixel = &mut image[(row * width * samples + col)];
|
||||||
|
*pixel = pixel.wrapping_add(prev_pixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rev_hpredict(image: DecodingResult, size: (u32, u32), color_type: ColorType) -> ImageResult<DecodingResult> {
|
||||||
|
let samples = match color_type {
|
||||||
|
ColorType::Gray(8) | ColorType::Gray(16) => 1,
|
||||||
|
ColorType::RGB(8) | ColorType::RGB(16) => 3,
|
||||||
|
ColorType::RGBA(8) | ColorType::RGBA(16) => 4,
|
||||||
|
_ => return Err(ImageError::UnsupportedError(format!(
|
||||||
|
"Horizontal predictor for {:?} is unsupported.", color_type
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
Ok(match image {
|
||||||
|
DecodingResult::U8(buf) => {
|
||||||
|
DecodingResult::U8(rev_hpredict_nsamp(buf, size, samples))
|
||||||
|
},
|
||||||
|
DecodingResult::U16(buf) => {
|
||||||
|
DecodingResult::U16(rev_hpredict_nsamp(buf, size, samples))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read + Seek> TIFFDecoder<R> {
|
||||||
|
/// Create a new decoder that decodes from the stream ```r```
|
||||||
|
pub fn new(r: R) -> ImageResult<TIFFDecoder<R>> {
|
||||||
|
TIFFDecoder {
|
||||||
|
reader: SmartReader::wrap(r, ByteOrder::LittleEndian),
|
||||||
|
byte_order: ByteOrder::LittleEndian,
|
||||||
|
next_ifd: None,
|
||||||
|
ifd: None,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
bits_per_sample: vec![1],
|
||||||
|
samples: 1,
|
||||||
|
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
|
||||||
|
compression_method: CompressionMethod::None
|
||||||
|
}.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_header(&mut self) -> ImageResult<()> {
|
||||||
|
let mut endianess = Vec::with_capacity(2);
|
||||||
|
try!(self.reader.by_ref().take(2).read_to_end(&mut endianess));
|
||||||
|
match &*endianess {
|
||||||
|
b"II" => {
|
||||||
|
self.byte_order = ByteOrder::LittleEndian;
|
||||||
|
self.reader.byte_order = ByteOrder::LittleEndian; },
|
||||||
|
b"MM" => {
|
||||||
|
self.byte_order = ByteOrder::BigEndian;
|
||||||
|
self.reader.byte_order = ByteOrder::BigEndian; },
|
||||||
|
_ => return Err(image::ImageError::FormatError(
|
||||||
|
"TIFF signature not found.".to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if try!(self.read_short()) != 42 {
|
||||||
|
return Err(image::ImageError::FormatError("TIFF signature invalid.".to_string()))
|
||||||
|
}
|
||||||
|
self.next_ifd = match try!(self.read_long()) {
|
||||||
|
0 => None,
|
||||||
|
n => Some(n)
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the decoder.
|
||||||
|
pub fn init(mut self) -> ImageResult<TIFFDecoder<R>> {
|
||||||
|
try!(self.read_header());
|
||||||
|
self.next_image()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads in the next image.
|
||||||
|
/// If there is no further image in the TIFF file a format error is returned.
|
||||||
|
/// To determine whether there are more images call `TIFFDecoder::more_images` instead.
|
||||||
|
pub fn next_image(mut self) -> ImageResult<TIFFDecoder<R>> {
|
||||||
|
self.ifd = Some(try!(self.read_ifd()));
|
||||||
|
self.width = try!(self.get_tag_u32(ifd::Tag::ImageWidth));
|
||||||
|
self.height = try!(self.get_tag_u32(ifd::Tag::ImageLength));
|
||||||
|
self.photometric_interpretation = match FromPrimitive::from_u32(
|
||||||
|
try!(self.get_tag_u32(ifd::Tag::PhotometricInterpretation))
|
||||||
|
) {
|
||||||
|
Some(val) => val,
|
||||||
|
None => return Err(image::ImageError::UnsupportedError(
|
||||||
|
"The image is using an unknown photometric interpretation.".to_string()
|
||||||
|
))
|
||||||
|
};
|
||||||
|
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(image::ImageError::UnsupportedError(
|
||||||
|
"Unknown compression method.".to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(val) = try!(self.find_tag_u32(ifd::Tag::SamplesPerPixel)) {
|
||||||
|
self.samples = val as u8
|
||||||
|
}
|
||||||
|
match self.samples {
|
||||||
|
1 => {
|
||||||
|
if let Some(val) = try!(self.find_tag_u32(ifd::Tag::BitsPerSample)) {
|
||||||
|
self.bits_per_sample = vec![val as u8]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 | 4 => {
|
||||||
|
if let Some(val) = try!(self.find_tag_u32_vec(ifd::Tag::BitsPerSample)) {
|
||||||
|
self.bits_per_sample = val.iter().map(|&v| v as u8).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
_ => return Err(image::ImageError::UnsupportedError(
|
||||||
|
format!("{} samples per pixel is supported.", self.samples)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if there is at least one more image available.
|
||||||
|
pub fn more_images(&self) -> bool {
|
||||||
|
match self.next_ifd {
|
||||||
|
Some(_) => true,
|
||||||
|
None => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the byte_order
|
||||||
|
pub fn byte_order(&self) -> ByteOrder {
|
||||||
|
self.byte_order
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a TIFF short value
|
||||||
|
#[inline]
|
||||||
|
pub fn read_short(&mut self) -> Result<u16, io::Error> {
|
||||||
|
self.reader.read_u16()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a TIFF long value
|
||||||
|
#[inline]
|
||||||
|
pub fn read_long(&mut self) -> Result<u32, io::Error> {
|
||||||
|
self.reader.read_u32()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a TIFF IFA offset/value field
|
||||||
|
#[inline]
|
||||||
|
pub fn read_offset(&mut self) -> Result<[u8; 4], io::Error> {
|
||||||
|
let mut val = [0; 4];
|
||||||
|
try!(self.reader.read_exact(&mut val));
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the cursor to the specified offset
|
||||||
|
#[inline]
|
||||||
|
pub fn goto_offset(&mut self, offset: u32) -> io::Result<()> {
|
||||||
|
self.reader.seek(io::SeekFrom::Start(u64::from(offset))).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a IFD entry.
|
||||||
|
// An IFD entry has four fields:
|
||||||
|
//
|
||||||
|
// Tag 2 bytes
|
||||||
|
// Type 2 bytes
|
||||||
|
// Count 4 bytes
|
||||||
|
// Value 4 bytes either a pointer the value itself
|
||||||
|
fn read_entry(&mut self) -> ImageResult<Option<(ifd::Tag, ifd::Entry)>> {
|
||||||
|
let tag = ifd::Tag::from_u16(try!(self.read_short()));
|
||||||
|
let type_: ifd::Type = match FromPrimitive::from_u16(try!(self.read_short())) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
// Unknown type. Skip this entry according to spec.
|
||||||
|
try!(self.read_long());
|
||||||
|
try!(self.read_long());
|
||||||
|
return Ok(None)
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some((tag, ifd::Entry::new(
|
||||||
|
type_,
|
||||||
|
try!(self.read_long()), // count
|
||||||
|
try!(self.read_offset()) // offset
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the next IFD
|
||||||
|
fn read_ifd(&mut self) -> ImageResult<Directory> {
|
||||||
|
let mut dir: Directory = HashMap::new();
|
||||||
|
match self.next_ifd {
|
||||||
|
None => return Err(image::ImageError::FormatError(
|
||||||
|
"Image file directory not found.".to_string())
|
||||||
|
),
|
||||||
|
Some(offset) => try!(self.goto_offset(offset))
|
||||||
|
}
|
||||||
|
for _ in 0..try!(self.read_short()) {
|
||||||
|
let (tag, entry) = match try!(self.read_entry()) {
|
||||||
|
Some(val) => val,
|
||||||
|
None => continue // Unknown data type in tag, skip
|
||||||
|
};
|
||||||
|
dir.insert(tag, entry);
|
||||||
|
}
|
||||||
|
self.next_ifd = match try!(self.read_long()) {
|
||||||
|
0 => None,
|
||||||
|
n => Some(n)
|
||||||
|
};
|
||||||
|
Ok(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to retrieve a tag.
|
||||||
|
/// Return `Ok(None)` if the tag is not present.
|
||||||
|
fn find_tag(&mut self, tag: ifd::Tag) -> ImageResult<Option<ifd::Value>> {
|
||||||
|
let ifd: &Directory = unsafe {
|
||||||
|
let ifd = self.ifd.as_ref().unwrap(); // Ok to fail
|
||||||
|
// Get a immutable borrow of self
|
||||||
|
// This is ok because entry val only changes the stream
|
||||||
|
// but not the directory.
|
||||||
|
mem::transmute_copy(&ifd)
|
||||||
|
};
|
||||||
|
match ifd.get(&tag) {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(entry) => Ok(Some(try!(entry.val(self))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to retrieve a tag and convert it to the desired type.
|
||||||
|
fn find_tag_u32(&mut self, tag: ifd::Tag) -> ImageResult<Option<u32>> {
|
||||||
|
match self.find_tag(tag)? {
|
||||||
|
Some(val) => val.into_u32().map(Some),
|
||||||
|
None => Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to retrieve a tag and convert it to the desired type.
|
||||||
|
fn find_tag_u32_vec(&mut self, tag: ifd::Tag) -> ImageResult<Option<Vec<u32>>> {
|
||||||
|
match self.find_tag(tag)? {
|
||||||
|
Some(val) => val.into_u32_vec().map(Some),
|
||||||
|
None => Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to retrieve a tag.
|
||||||
|
/// Returns an error if the tag is not present
|
||||||
|
fn get_tag(&mut self, tag: ifd::Tag) -> ImageResult<ifd::Value> {
|
||||||
|
match try!(self.find_tag(tag)) {
|
||||||
|
Some(val) => Ok(val),
|
||||||
|
None => Err(::image::ImageError::FormatError(format!(
|
||||||
|
"Required tag `{:?}` not found.", tag
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to retrieve a tag and convert it to the desired type.
|
||||||
|
fn get_tag_u32(&mut self, tag: ifd::Tag) -> ImageResult<u32> {
|
||||||
|
self.get_tag(tag)?.into_u32()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to retrieve a tag and convert it to the desired type.
|
||||||
|
fn get_tag_u32_vec(&mut self, tag: ifd::Tag) -> ImageResult<Vec<u32>> {
|
||||||
|
self.get_tag(tag)?.into_u32_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decompresses the strip into the supplied buffer.
|
||||||
|
/// Returns the number of bytes read.
|
||||||
|
fn expand_strip<'a>(&mut self, buffer: DecodingBuffer<'a>, offset: u32, length: u32, max_uncompressed_length: usize) -> ImageResult<usize> {
|
||||||
|
let color_type = try!(self.colortype());
|
||||||
|
try!(self.goto_offset(offset));
|
||||||
|
let (bytes, mut reader): (usize, Box<EndianReader>) = match self.compression_method {
|
||||||
|
CompressionMethod::None => {
|
||||||
|
let order = self.reader.byte_order;
|
||||||
|
(length as usize, Box::new(SmartReader::wrap(&mut self.reader, order)))
|
||||||
|
},
|
||||||
|
CompressionMethod::LZW => {
|
||||||
|
let (bytes, reader) = try!(LZWReader::new(&mut self.reader, length as usize, max_uncompressed_length));
|
||||||
|
(bytes, Box::new(reader))
|
||||||
|
},
|
||||||
|
CompressionMethod::PackBits => {
|
||||||
|
let order = self.reader.byte_order;
|
||||||
|
let (bytes, reader) = try!(PackBitsReader::new(&mut self.reader, order, length as usize));
|
||||||
|
(bytes, Box::new(reader))
|
||||||
|
},
|
||||||
|
method => return Err(::image::ImageError::UnsupportedError(format!(
|
||||||
|
"Compression method {:?} is unsupported", method
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
Ok(match (color_type, buffer) {
|
||||||
|
(ColorType:: RGB(8), DecodingBuffer::U8(ref mut buffer)) |
|
||||||
|
(ColorType::RGBA(8), DecodingBuffer::U8(ref mut buffer)) => {
|
||||||
|
try!(reader.read(&mut buffer[..bytes]))
|
||||||
|
}
|
||||||
|
(ColorType::RGBA(16), DecodingBuffer::U16(ref mut buffer)) |
|
||||||
|
(ColorType:: RGB(16), DecodingBuffer::U16(ref mut buffer)) => {
|
||||||
|
for datum in buffer[..bytes/2].iter_mut() {
|
||||||
|
*datum = try!(reader.read_u16())
|
||||||
|
}
|
||||||
|
bytes/2
|
||||||
|
}
|
||||||
|
(ColorType::Gray(16), DecodingBuffer::U16(ref mut buffer)) => {
|
||||||
|
for datum in buffer[..bytes/2].iter_mut() {
|
||||||
|
*datum = try!(reader.read_u16());
|
||||||
|
if self.photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
|
*datum = 0xffff - *datum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes/2
|
||||||
|
}
|
||||||
|
(ColorType::Gray(n), DecodingBuffer::U8(ref mut buffer)) if n <= 8 => {
|
||||||
|
try!(reader.read_exact(&mut buffer[..bytes]));
|
||||||
|
if self.photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||||
|
for byte in buffer[..bytes].iter_mut() {
|
||||||
|
*byte = 0xff - *byte
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
(type_, _) => return Err(::image::ImageError::UnsupportedError(format!(
|
||||||
|
"Color type {:?} is unsupported", type_
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read + Seek> ImageDecoder for TIFFDecoder<R> {
|
||||||
|
fn dimensions(&mut self) -> ImageResult<(u32, u32)> {
|
||||||
|
Ok((self.width, self.height))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn colortype(&mut self) -> ImageResult<ColorType> {
|
||||||
|
match self.photometric_interpretation {
|
||||||
|
// TODO: catch also [ 8, 8, 8, _] this does not work due to a bug in rust atm
|
||||||
|
PhotometricInterpretation::RGB if self.bits_per_sample == [8, 8, 8, 8] => Ok(ColorType::RGBA(8)),
|
||||||
|
PhotometricInterpretation::RGB if self.bits_per_sample == [8, 8, 8] => Ok(ColorType::RGB(8)),
|
||||||
|
PhotometricInterpretation::RGB if self.bits_per_sample == [16, 16, 16, 16] => Ok(ColorType::RGBA(16)),
|
||||||
|
PhotometricInterpretation::RGB if self.bits_per_sample == [16, 16, 16] => Ok(ColorType::RGB(16)),
|
||||||
|
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero
|
||||||
|
if self.bits_per_sample.len() == 1 => Ok(ColorType::Gray(self.bits_per_sample[0])),
|
||||||
|
|
||||||
|
_ => Err(::image::ImageError::UnsupportedError(format!(
|
||||||
|
"{:?} with {:?} bits per sample is unsupported", self.bits_per_sample, self.photometric_interpretation
|
||||||
|
))) // TODO: this is bad we should not fail at this point}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_len(&mut self) -> ImageResult<usize> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_scanline(&mut self, _: &mut [u8]) -> ImageResult<u32> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_image(&mut self) -> ImageResult<DecodingResult> {
|
||||||
|
let bits_per_pixel: u8 = self.bits_per_sample.iter().cloned().sum();
|
||||||
|
let scanline_size_bits = self.width as usize * bits_per_pixel as usize;
|
||||||
|
let scanline_size = (scanline_size_bits + 7) / 8;
|
||||||
|
let rows_per_strip = self.get_tag_u32(ifd::Tag::RowsPerStrip)
|
||||||
|
.unwrap_or(self.height) as usize;
|
||||||
|
let buffer_size =
|
||||||
|
self.width as usize
|
||||||
|
* self.height as usize
|
||||||
|
* self.bits_per_sample.iter().count();
|
||||||
|
let mut result = match self.bits_per_sample.iter().cloned().max().unwrap_or(8) {
|
||||||
|
n if n <= 8 => DecodingResult::U8(Vec::with_capacity(buffer_size)),
|
||||||
|
n if n <= 16 => DecodingResult::U16(Vec::with_capacity(buffer_size)),
|
||||||
|
n => return Err(
|
||||||
|
ImageError::UnsupportedError(
|
||||||
|
format!("{} bits per channel not supported", n)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if let Ok(config) = self.get_tag_u32(ifd::Tag::PlanarConfiguration) {
|
||||||
|
match FromPrimitive::from_u32(config) {
|
||||||
|
Some(PlanarConfiguration::Chunky) => {},
|
||||||
|
config => return Err(ImageError::UnsupportedError(
|
||||||
|
format!("Unsupported planar configuration “{:?}”.", config)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Safe since the uninitialized values are never read.
|
||||||
|
match result {
|
||||||
|
DecodingResult::U8(ref mut buffer) =>
|
||||||
|
unsafe { buffer.set_len(buffer_size) },
|
||||||
|
DecodingResult::U16(ref mut buffer) =>
|
||||||
|
unsafe { buffer.set_len(buffer_size) },
|
||||||
|
}
|
||||||
|
let mut units_read = 0;
|
||||||
|
for (i, (&offset, &byte_count)) in try!(self.get_tag_u32_vec(ifd::Tag::StripOffsets))
|
||||||
|
.iter().zip(try!(self.get_tag_u32_vec(ifd::Tag::StripByteCounts)).iter()).enumerate() {
|
||||||
|
let uncompressed_strip_size = scanline_size
|
||||||
|
* (self.height as usize - i * rows_per_strip);
|
||||||
|
|
||||||
|
units_read += match result {
|
||||||
|
DecodingResult::U8(ref mut buffer) => {
|
||||||
|
try!(self.expand_strip(
|
||||||
|
DecodingBuffer::U8(&mut buffer[units_read..]),
|
||||||
|
offset, byte_count, uncompressed_strip_size
|
||||||
|
))
|
||||||
|
},
|
||||||
|
DecodingResult::U16(ref mut buffer) => {
|
||||||
|
try!(self.expand_strip(
|
||||||
|
DecodingBuffer::U16(&mut buffer[units_read..]),
|
||||||
|
offset, byte_count, uncompressed_strip_size
|
||||||
|
))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if units_read == buffer_size {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Shrink length such that the uninitialized memory is not exposed.
|
||||||
|
if units_read < buffer_size {
|
||||||
|
match result {
|
||||||
|
DecodingResult::U8(ref mut buffer) =>
|
||||||
|
unsafe { buffer.set_len(units_read) },
|
||||||
|
DecodingResult::U16(ref mut buffer) =>
|
||||||
|
unsafe { buffer.set_len(units_read) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(predictor) = self.get_tag_u32(ifd::Tag::Predictor) {
|
||||||
|
result = match FromPrimitive::from_u32(predictor) {
|
||||||
|
Some(Predictor::None) => result,
|
||||||
|
Some(Predictor::Horizontal) => {
|
||||||
|
try!(rev_hpredict(
|
||||||
|
result,
|
||||||
|
try!(self.dimensions()),
|
||||||
|
try!(self.colortype())
|
||||||
|
))
|
||||||
|
},
|
||||||
|
None => return Err(ImageError::FormatError(
|
||||||
|
format!("Unknown predictor “{}” encountered", predictor)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
//! All IO functionality needed for TIFF decoding
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::io::{Read, Seek};
|
||||||
|
use byteorder::{ReadBytesExt, BigEndian, LittleEndian};
|
||||||
|
use lzw;
|
||||||
|
|
||||||
|
/// Byte order of the TIFF file.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum ByteOrder {
|
||||||
|
/// little endian byte order
|
||||||
|
LittleEndian,
|
||||||
|
/// big endian byte order
|
||||||
|
BigEndian
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Reader that is aware of the byte order.
|
||||||
|
pub trait EndianReader: Read {
|
||||||
|
/// Byte order that should be adhered to
|
||||||
|
fn byte_order(&self) -> ByteOrder;
|
||||||
|
|
||||||
|
/// Reads an u16
|
||||||
|
#[inline(always)]
|
||||||
|
fn read_u16(&mut self) -> Result<u16, io::Error> {
|
||||||
|
match self.byte_order() {
|
||||||
|
ByteOrder::LittleEndian => <Self as ReadBytesExt>::read_u16::<LittleEndian>(self),
|
||||||
|
ByteOrder::BigEndian => <Self as ReadBytesExt>::read_u16::<BigEndian>(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads an u32
|
||||||
|
#[inline(always)]
|
||||||
|
fn read_u32(&mut self) -> Result<u32, io::Error> {
|
||||||
|
match self.byte_order() {
|
||||||
|
ByteOrder::LittleEndian => <Self as ReadBytesExt>::read_u32::<LittleEndian>(self),
|
||||||
|
ByteOrder::BigEndian => <Self as ReadBytesExt>::read_u32::<BigEndian>(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reader that decompresses LZW streams
|
||||||
|
pub struct LZWReader {
|
||||||
|
buffer: io::Cursor<Vec<u8>>,
|
||||||
|
byte_order: ByteOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LZWReader {
|
||||||
|
/// Wraps a reader
|
||||||
|
pub fn new<R>(reader: &mut SmartReader<R>, compressed_length: usize, max_uncompressed_length: usize) -> io::Result<(usize, LZWReader)> where R: Read + Seek {
|
||||||
|
let order = reader.byte_order;
|
||||||
|
let mut compressed = vec![0; compressed_length as usize];
|
||||||
|
try!(reader.read_exact(&mut compressed[..]));
|
||||||
|
let mut uncompressed = Vec::with_capacity(max_uncompressed_length);
|
||||||
|
let mut decoder = lzw::DecoderEarlyChange::new(lzw::MsbReader::new(), 8);
|
||||||
|
let mut bytes_read = 0;
|
||||||
|
while bytes_read < compressed_length && uncompressed.len() < max_uncompressed_length {
|
||||||
|
let (len, bytes) = try!(decoder.decode_bytes(&compressed[bytes_read..]));
|
||||||
|
bytes_read += len;
|
||||||
|
uncompressed.extend_from_slice(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = uncompressed.len();
|
||||||
|
Ok((bytes, LZWReader {
|
||||||
|
buffer: io::Cursor::new(uncompressed),
|
||||||
|
byte_order: order
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for LZWReader {
|
||||||
|
#[inline]
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.buffer.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EndianReader for LZWReader {
|
||||||
|
#[inline(always)]
|
||||||
|
fn byte_order(&self) -> ByteOrder {
|
||||||
|
self.byte_order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reader that unpacks Apple's `PackBits` format
|
||||||
|
pub struct PackBitsReader {
|
||||||
|
buffer: io::Cursor<Vec<u8>>,
|
||||||
|
byte_order: ByteOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackBitsReader {
|
||||||
|
/// Wraps a reader
|
||||||
|
pub fn new<R: Read + Seek>(mut reader: R, byte_order: ByteOrder, length: usize) -> io::Result<(usize, PackBitsReader)> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let mut read: usize = 0;
|
||||||
|
while read < length {
|
||||||
|
read += try!(read_packbits_run(&mut reader, &mut buffer));
|
||||||
|
}
|
||||||
|
Ok((buffer.len(), PackBitsReader {
|
||||||
|
buffer: io::Cursor::new(buffer),
|
||||||
|
byte_order: byte_order
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_packbits_run<R: Read + Seek>(reader: &mut R, buffer: &mut Vec<u8>) -> io::Result<usize> {
|
||||||
|
let mut header: [u8; 1] = [0];
|
||||||
|
|
||||||
|
let bytes = try!(reader.read(&mut header));
|
||||||
|
|
||||||
|
match bytes {
|
||||||
|
0 => Ok(0),
|
||||||
|
_ => match header[0] as i8 {
|
||||||
|
-128 => Ok(1),
|
||||||
|
h if h >= -127 && h <= -1 => {
|
||||||
|
let new_len = buffer.len() + (1 - h as isize) as usize;
|
||||||
|
try!(reader.read_exact(&mut header));
|
||||||
|
buffer.resize(new_len, header[0]);
|
||||||
|
Ok(2)
|
||||||
|
},
|
||||||
|
h => {
|
||||||
|
let num_vals = h as usize + 1;
|
||||||
|
let start = buffer.len();
|
||||||
|
buffer.resize(start + num_vals, 0);
|
||||||
|
try!(reader.read_exact(&mut buffer[start..]));
|
||||||
|
Ok(num_vals + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for PackBitsReader {
|
||||||
|
#[inline]
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.buffer.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EndianReader for PackBitsReader {
|
||||||
|
#[inline(always)]
|
||||||
|
fn byte_order(&self) -> ByteOrder {
|
||||||
|
self.byte_order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reader that is aware of the byte order.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SmartReader<R> where R: Read + Seek {
|
||||||
|
reader: R,
|
||||||
|
pub byte_order: ByteOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> SmartReader<R> where R: Read + Seek {
|
||||||
|
/// Wraps a reader
|
||||||
|
pub fn wrap(reader: R, byte_order: ByteOrder) -> SmartReader<R> {
|
||||||
|
SmartReader {
|
||||||
|
reader: reader,
|
||||||
|
byte_order: byte_order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> EndianReader for SmartReader<R> where R: Read + Seek {
|
||||||
|
#[inline(always)]
|
||||||
|
fn byte_order(&self) -> ByteOrder {
|
||||||
|
self.byte_order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read + Seek> Read for SmartReader<R> {
|
||||||
|
#[inline]
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.reader.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read + Seek> Seek for SmartReader<R> {
|
||||||
|
#[inline]
|
||||||
|
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
|
||||||
|
self.reader.seek(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packbits() {
|
||||||
|
let encoded = vec![0xFE, 0xAA, 0x02, 0x80, 0x00, 0x2A, 0xFD, 0xAA,
|
||||||
|
0x03, 0x80, 0x00, 0x2A, 0x22, 0xF7, 0xAA];
|
||||||
|
let encoded_len = encoded.len();
|
||||||
|
|
||||||
|
let buff = io::Cursor::new(encoded);
|
||||||
|
let (_, mut decoder) = PackBitsReader::new(buff, ByteOrder::LittleEndian, encoded_len).unwrap();
|
||||||
|
|
||||||
|
let mut decoded = Vec::new();
|
||||||
|
decoder.read_to_end(&mut decoded).unwrap();
|
||||||
|
|
||||||
|
let expected = vec![0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0xAA, 0xAA,
|
||||||
|
0xAA, 0xAA, 0x80, 0x00, 0x2A, 0x22, 0xAA, 0xAA,
|
||||||
|
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA];
|
||||||
|
assert_eq!(decoded, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
//! Decoding and Encoding of TIFF Images
|
||||||
|
//!
|
||||||
|
//! TIFF (Tagged Image File Format) is a versatile image format that supports
|
||||||
|
//! lossless and lossy compression.
|
||||||
|
//!
|
||||||
|
//! # Related Links
|
||||||
|
//! * <http://partners.adobe.com/public/developer/tiff/index.html> - The TIFF specification
|
||||||
|
|
||||||
|
extern crate byteorder;
|
||||||
|
extern crate lzw;
|
||||||
|
extern crate num_traits;
|
||||||
|
|
||||||
|
pub use self::decoder::TIFFDecoder;
|
||||||
|
|
||||||
|
mod decoder;
|
||||||
Reference in new issue
Block a user