diff --git a/src/ImageSharp.Formats.Tiff/ImageSharp.Formats.Tiff.csproj b/src/ImageSharp.Formats.Tiff/ImageSharp.Formats.Tiff.csproj new file mode 100644 index 0000000000..23103c903b --- /dev/null +++ b/src/ImageSharp.Formats.Tiff/ImageSharp.Formats.Tiff.csproj @@ -0,0 +1,7 @@ + + + + netstandard1.1 + + + diff --git a/src/ImageSharp.Formats.Tiff/TiffTags.cs b/src/ImageSharp.Formats.Tiff/TiffTags.cs new file mode 100644 index 0000000000..db4087d58c --- /dev/null +++ b/src/ImageSharp.Formats.Tiff/TiffTags.cs @@ -0,0 +1,205 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + /// + /// Constants representing tag IDs in the Tiff file-format. + /// + public class TiffTags + { + // Section 8: Baseline Fields + + public const int Artist = 315; + public const int BitsPerSample = 258; + public const int CellLength = 265; + public const int CellWidth = 264; + public const int ColorMap = 320; + public const int Compression = 259; + public const int Copyright = 33432; + public const int DateTime = 306; + public const int ExtraSamples = 338; + public const int FillOrder = 266; + public const int FreeByteCounts = 289; + public const int FreeOffsets = 288; + public const int GrayResponseCurve = 291; + public const int GrayResponseUnit = 290; + public const int HostComputer = 316; + public const int ImageDescription = 270; + public const int ImageLength = 257; + public const int ImageWidth = 256; + public const int Make = 271; + public const int MaxSampleValue = 281; + public const int MinSampleValue = 280; + public const int Model = 272; + public const int NewSubfileType = 254; + public const int Orientation = 274; + public const int PhotometricInterpretation = 262; + public const int PlanarConfiguration = 284; + public const int ResolutionUnit = 296; + public const int RowsPerStrip = 278; + public const int SamplesPerPixel = 277; + public const int Software = 305; + public const int StripByteCounts = 279; + public const int StripOffsets = 273; + public const int SubfileType = 255; + public const int Threshholding = 263; + public const int XResolution = 282; + public const int YResolution = 283; + + // Section 11: CCITT Bilevel Encodings + + public const int T4Options = 292; + public const int T6Options = 293; + + // Section 12: Document Storage and Retrieval + + public const int DocumentName = 269; + public const int PageName = 285; + public const int PageNumber = 297; + public const int XPosition = 286; + public const int YPosition = 287; + + // Section 14: Differencing Predictor + + public const int Predictor = 317; + + // Section 15: Tiled Images + + public const int TileWidth = 322; + public const int TileLength = 323; + public const int TileOffsets = 324; + public const int TileByteCounts = 325; + + // Section 16: CMYK Images + + public const int InkSet = 332; + public const int NumberOfInks = 334; + public const int InkNames = 333; + public const int DotRange = 336; + public const int TargetPrinter = 337; + + // Section 17: Halftone Hints + + public const int HalftoneHints = 321; + + // Section 19: Data Sample Format + + public const int SampleFormat = 339; + public const int SMinSampleValue = 340; + public const int SMaxSampleValue = 341; + + // Section 20: RGB Image Colorimetry + + public const int WhitePoint = 318; + public const int PrimaryChromaticities = 319; + public const int TransferFunction = 301; + public const int TransferRange = 342; + public const int ReferenceBlackWhite = 532; + + // Section 21: YCbCr Images + + public const int YCbCrCoefficients = 529; + public const int YCbCrSubSampling = 530; + public const int YCbCrPositioning = 531; + + // Section 22: JPEG Compression + + public const int JpegProc = 512; + public const int JpegInterchangeFormat = 513; + public const int JpegInterchangeFormatLength = 514; + public const int JpegRestartInterval = 515; + public const int JpegLosslessPredictors = 517; + public const int JpegPointTransforms = 518; + public const int JpegQTables = 519; + public const int JpegDCTables = 520; + public const int JpegACTables = 521; + + // TIFF Supplement 1: Adobe Pagemaker 6.0 + + public const int SubIFDs = 330; + public const int ClipPath = 343; + public const int XClipPathUnits = 344; + public const int YClipPathUnits = 345; + public const int Indexed = 346; + public const int ImageID = 32781; + public const int OpiProxy = 351; + + // TIFF Supplement 2: Adobe Photoshop + + public const int ImageSourceData = 37724; + + // TIFF/EP Specification: Additional Tags + + public const int JPEGTables = 0x015B; + public const int CFARepeatPatternDim = 0x828D; + public const int BatteryLevel = 0x828F; + public const int Interlace = 0x8829; + public const int TimeZoneOffset = 0x882A; + public const int SelfTimerMode = 0x882B; + public const int Noise = 0x920D; + public const int ImageNumber = 0x9211; + public const int SecurityClassification = 0x9212; + public const int ImageHistory = 0x9213; + public const int TiffEPStandardID = 0x9216; + + // TIFF-F/FX Specification (http://www.ietf.org/rfc/rfc2301.txt) + + public const int BadFaxLines = 326; + public const int CleanFaxData = 327; + public const int ConsecutiveBadFaxLines = 328; + public const int GlobalParametersIFD = 400; + public const int ProfileType = 401; + public const int FaxProfile = 402; + public const int CodingMethod = 403; + public const int VersionYear = 404; + public const int ModeNumber = 405; + public const int Decode = 433; + public const int DefaultImageColor = 434; + public const int StripRowCounts = 559; + public const int ImageLayer = 34732; + + // Embedded Metadata + + public const int Xmp = 700; + public const int Iptc = 33723; + public const int Photoshop = 34377; + public const int ExifIFD = 34665; + public const int GpsIFD = 34853; + public const int InteroperabilityIFD = 40965; + + // Other Private TIFF tags (http://www.awaresystems.be/imaging/tiff/tifftags/private.html) + + public const int WangAnnotation = 32932; + public const int MDFileTag = 33445; + public const int MDScalePixel = 33446; + public const int MDColorTable = 33447; + public const int MDLabName = 33448; + public const int MDSampleInfo = 33449; + public const int MDPrepDate = 33450; + public const int MDPrepTime = 33451; + public const int MDFileUnits = 33452; + public const int ModelPixelScaleTag = 33550; + public const int IngrPacketDataTag = 33918; + public const int IngrFlagRegisters = 33919; + public const int IrasBTransformationMatrix = 33920; + public const int ModelTiePointTag = 33922; + public const int ModelTransformationTag = 34264; + public const int IccProfile = 34675; + public const int GeoKeyDirectoryTag = 34735; + public const int GeoDoubleParamsTag = 34736; + public const int GeoAsciiParamsTag = 34737; + public const int HylaFAXFaxRecvParams = 34908; + public const int HylaFAXFaxSubAddress = 34909; + public const int HylaFAXFaxRecvTime = 34910; + public const int GdalMetadata = 42112; + public const int GdalNodata = 42113; + public const int OceScanjobDescription = 50215; + public const int OceApplicationSelector = 50216; + public const int OceIdentificationNumber = 50217; + public const int OceImageLogicCharacteristics = 50218; + public const int AliasLayerMetadata = 50784; + } +} \ No newline at end of file diff --git a/src/ImageSharp.Formats.Tiff/TiffType.cs b/src/ImageSharp.Formats.Tiff/TiffType.cs new file mode 100644 index 0000000000..1bb7f6cfbb --- /dev/null +++ b/src/ImageSharp.Formats.Tiff/TiffType.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + /// + /// Enumeration representing the data types understood by the Tiff file-format. + /// + public enum TiffType + { + Byte = 1, + Ascii = 2, + Short = 3, + Long = 4, + Rational = 5, + SByte = 6, + Undefined = 7, + SShort = 8, + SLong = 9, + SRational = 10, + Float = 11, + Double = 12, + Ifd = 13 + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/ImageSharp.Formats.Tiff.Tests.csproj b/tests/ImageSharp.Formats.Tiff.Tests/ImageSharp.Formats.Tiff.Tests.csproj new file mode 100644 index 0000000000..a20f1fd99f --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/ImageSharp.Formats.Tiff.Tests.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp1.0 + + + + + + + + + + + + + diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/ByteArrayUtility.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/ByteArrayUtility.cs new file mode 100644 index 0000000000..8021f53302 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/ByteArrayUtility.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + + public static class ByteArrayUtility + { + public static byte[] WithByteOrder(this byte[] bytes, bool isLittleEndian) + { + if (BitConverter.IsLittleEndian != isLittleEndian) + { + byte[] reversedBytes = new byte[bytes.Length]; + Array.Copy(bytes, reversedBytes, bytes.Length); + Array.Reverse(reversedBytes); + return reversedBytes; + } + else + { + return bytes; + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/ByteBuffer.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/ByteBuffer.cs new file mode 100644 index 0000000000..290c942f8b --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/ByteBuffer.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + + public class ByteBuffer + { + List bytes = new List(); + bool isLittleEndian; + + public ByteBuffer(bool isLittleEndian) + { + this.isLittleEndian = isLittleEndian; + } + + public void AddByte(byte value) + { + bytes.Add(value); + } + + public void AddUInt16(ushort value) + { + bytes.AddRange(BitConverter.GetBytes(value).WithByteOrder(isLittleEndian)); + } + + public void AddUInt32(uint value) + { + bytes.AddRange(BitConverter.GetBytes(value).WithByteOrder(isLittleEndian)); + } + + public byte[] ToArray() + { + return bytes.ToArray(); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/ITiffGenDataSource.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/ITiffGenDataSource.cs new file mode 100644 index 0000000000..33bf999244 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/ITiffGenDataSource.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + + /// + /// An interface for any class within the Tiff generator that produces data to be included in the file. + /// + public interface ITiffGenDataSource + { + IEnumerable GetData(bool isLittleEndian); + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenDataBlock.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenDataBlock.cs new file mode 100644 index 0000000000..bbce120541 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenDataBlock.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + + /// + /// A utility data structure to represent an independent block of data in a Tiff file. + /// These may be located in any order within a Tiff file. + /// + public class TiffGenDataBlock + { + public TiffGenDataBlock(byte[] bytes) + { + this.Bytes = bytes; + this.References = new List(); + } + + public byte[] Bytes { get; } + public IList References { get; } + + public void AddReference(byte[] bytes, int offset) + { + References.Add(new TiffGenDataReference(bytes, offset)); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenDataReference.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenDataReference.cs new file mode 100644 index 0000000000..ec4c0c5df4 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenDataReference.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + + /// + /// A utility data structure to represent a reference from one block of data to another in a Tiff file. + /// + public class TiffGenDataReference + { + public TiffGenDataReference(byte[] bytes, int offset) + { + this.Bytes = bytes; + this.Offset = offset; + } + + public byte[] Bytes { get; } + public int Offset { get; } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenEntry.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenEntry.cs new file mode 100644 index 0000000000..4f0ff868b1 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenEntry.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using ImageSharp.Formats; + + /// + /// A utility data structure to represent Tiff IFD entries in unit tests. + /// + public abstract class TiffGenEntry : ITiffGenDataSource + { + private TiffGenEntry(ushort tag, TiffType type) + { + this.Tag = tag; + this.Type = type; + } + + public ushort Tag { get; } + public TiffType Type { get; } + + public abstract IEnumerable GetData(bool isLittleEndian); + + public static TiffGenEntry Ascii(ushort tag, string value) + { + return new TiffGenEntryAscii(tag, value); + } + + public static TiffGenEntry Integer(ushort tag, TiffType type, int value) + { + return TiffGenEntry.Integer(tag, type, new int[] {value}); + } + + public static TiffGenEntry Integer(ushort tag, TiffType type, int[] value) + { + if (type != TiffType.Byte && type != TiffType.Short && type != TiffType.Long && + type != TiffType.SByte && type != TiffType.SShort && type != TiffType.SLong) + throw new ArgumentException(nameof(type), "The specified type is not an integer type."); + + return new TiffGenEntryInteger(tag, type, value); + } + + private class TiffGenEntryAscii : TiffGenEntry + { + public TiffGenEntryAscii(ushort tag, string value) : base(tag, TiffType.Ascii) + { + this.Value = value; + } + + public string Value { get; } + + public override IEnumerable GetData(bool isLittleEndian) + { + byte[] bytes = Encoding.ASCII.GetBytes($"{Value}\0"); + return new[] { new TiffGenDataBlock(bytes) }; + } + } + + private class TiffGenEntryInteger : TiffGenEntry + { + public TiffGenEntryInteger(ushort tag, TiffType type, int[] value) : base(tag, type) + { + this.Value = value; + } + + public int[] Value { get; } + + public override IEnumerable GetData(bool isLittleEndian) + { + byte[] bytes = GetBytes().SelectMany(b => b.WithByteOrder(isLittleEndian)).ToArray(); + return new[] { new TiffGenDataBlock(bytes) }; + } + + private IEnumerable GetBytes() + { + switch (Type) + { + case TiffType.Byte: + return Value.Select(i => new byte[] { (byte)i }); + case TiffType.Short: + return Value.Select(i => BitConverter.GetBytes((ushort)i)); + case TiffType.Long: + return Value.Select(i => BitConverter.GetBytes((uint)i)); + case TiffType.SByte: + return Value.Select(i => BitConverter.GetBytes((sbyte)i)); + case TiffType.SShort: + return Value.Select(i => BitConverter.GetBytes((short)i)); + case TiffType.SLong: + return Value.Select(i => BitConverter.GetBytes((int)i)); + default: + throw new InvalidOperationException(); + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenExtensions.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenExtensions.cs new file mode 100644 index 0000000000..87ba621219 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenExtensions.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.IO; + using System.Linq; + + /// + /// A utility class for generating in-memory Tiff files for use in unit tests. + /// + public static class TiffGenExtensions + { + public static byte[] ToBytes(this ITiffGenDataSource dataSource, bool isLittleEndian) + { + var dataBlocks = dataSource.GetData(isLittleEndian); + + int offset = 0; + + foreach (var dataBlock in dataBlocks) + { + byte[] offsetBytes = BitConverter.GetBytes(offset).WithByteOrder(isLittleEndian); + + foreach (var reference in dataBlock.References) + { + reference.Bytes[reference.Offset + 0] = offsetBytes[0]; + reference.Bytes[reference.Offset + 1] = offsetBytes[1]; + reference.Bytes[reference.Offset + 2] = offsetBytes[2]; + reference.Bytes[reference.Offset + 3] = offsetBytes[3]; + } + + offset += dataBlock.Bytes.Length; + } + + return dataBlocks.SelectMany(b => b.Bytes).ToArray(); + } + + public static Stream ToStream(this ITiffGenDataSource dataSource, bool isLittleEndian) + { + var bytes = dataSource.ToBytes(isLittleEndian); + return new MemoryStream(bytes); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenHeader.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenHeader.cs new file mode 100644 index 0000000000..b270ff2081 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenHeader.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// A utility data structure to represent a Tiff file-header. + /// + public class TiffGenHeader : ITiffGenDataSource + { + public TiffGenHeader() + { + this.MagicNumber = 42; + } + + public ushort? ByteOrderMarker { get; set; } + public ushort MagicNumber { get; set; } + public TiffGenIfd FirstIfd { get; set; } + + public IEnumerable GetData(bool isLittleEndian) + { + ByteBuffer bytes = new ByteBuffer(isLittleEndian); + + bytes.AddUInt16(ByteOrderMarker ?? (isLittleEndian ? (ushort)0x4949 : (ushort)0x4D4D)); + bytes.AddUInt16(MagicNumber); + bytes.AddUInt32(0); + + var headerData = new TiffGenDataBlock(bytes.ToArray()); + var firstIfdData = FirstIfd.GetData(isLittleEndian); + + firstIfdData.First().AddReference(headerData.Bytes, 4); + + return new [] { headerData }.Concat(firstIfdData); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfd.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfd.cs new file mode 100644 index 0000000000..2e745df369 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfd.cs @@ -0,0 +1,91 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// A utility data structure to represent Tiff IFDs in unit tests. + /// + public class TiffGenIfd : ITiffGenDataSource + { + public TiffGenIfd() + { + this.Entries = new List(); + } + + public List Entries { get; } + public TiffGenIfd NextIfd { get; set; } + + public IEnumerable GetData(bool isLittleEndian) + { + ByteBuffer bytes = new ByteBuffer(isLittleEndian); + List dataBlocks = new List(); + List> entryReferences = new List>(); + + // Add the entry count + + bytes.AddUInt16((ushort)Entries.Count); + + // Add all IFD entries + + int entryOffset = 2; + + foreach (var entry in Entries) + { + var entryData = entry.GetData(isLittleEndian); + var entryBytes = entryData.First().Bytes; + var entryCount = entryBytes.Length; + + bytes.AddUInt16(entry.Tag); + bytes.AddUInt16((ushort)entry.Type); + bytes.AddUInt32((uint)entryCount); + + if (entryCount <=4) + { + bytes.AddByte(entryCount > 0 ? entryBytes[0] : (byte)0); + bytes.AddByte(entryCount > 1 ? entryBytes[1] : (byte)0); + bytes.AddByte(entryCount > 2 ? entryBytes[2] : (byte)0); + bytes.AddByte(entryCount > 3 ? entryBytes[3] : (byte)0); + + dataBlocks.AddRange(entryData.Skip(1)); + } + else + { + bytes.AddUInt32(0); + dataBlocks.AddRange(entryData); + entryReferences.Add(Tuple.Create(entryData.First(), entryOffset + 8)); + } + + entryOffset += 12; + } + + // Add reference to next IFD + + bytes.AddUInt32(0); + + // Build the data + + var ifdData = new TiffGenDataBlock(bytes.ToArray()); + + foreach (var entryReference in entryReferences) + { + entryReference.Item1.AddReference(ifdData.Bytes, entryReference.Item2); + } + + IEnumerable nextIfdData = new TiffGenDataBlock[0]; + if (NextIfd != null) + { + nextIfdData = NextIfd.GetData(isLittleEndian); + nextIfdData.First().AddReference(ifdData.Bytes, ifdData.Bytes.Length - 4); + } + + return new [] { ifdData }.Concat(dataBlocks).Concat(nextIfdData); + } + } +} \ No newline at end of file