From 4ec2fe9aa5aaf1936e31d95a0c41e733f1c88b16 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 28 Oct 2016 00:08:36 +1100 Subject: [PATCH] Don't adaptively encode indexed pngs Former-commit-id: fbb4508b15ccb81f78bae6cb0e969e6ee7a34350 --- .../Formats/Jpg/JpegDecoderCore.cs | 2100 +++++++++++++++++ .../Jpg/JpegDecoderCore.cs.REMOVED.git-id | 1 - .../Formats/Png/PngEncoderCore.cs | 28 +- .../TestImages/Formats/Bmp/Car.bmp | 3 + .../Formats/Bmp/Car.bmp.REMOVED.git-id | 1 - .../TestImages/Formats/Bmp/neg_height.bmp | 3 + .../Formats/Bmp/neg_height.bmp.REMOVED.git-id | 1 - .../TestImages/Formats/Jpg/Calliphora.jpg | 3 + .../Formats/Jpg/Calliphora.jpg.REMOVED.git-id | 1 - .../TestImages/Formats/Jpg/Floorplan.jpeg | 3 + .../Formats/Jpg/Floorplan.jpeg.REMOVED.git-id | 1 - .../TestImages/Formats/Jpg/cmyk.jpg | 3 + .../Formats/Jpg/cmyk.jpg.REMOVED.git-id | 1 - .../Formats/Jpg/gamma_dalai_lama_gray.jpg | 3 + .../gamma_dalai_lama_gray.jpg.REMOVED.git-id | 1 - .../TestImages/Formats/Png/blur.png | 3 + .../Formats/Png/blur.png.REMOVED.git-id | 1 - .../TestImages/Formats/Png/splash.png | 3 + .../Formats/Png/splash.png.REMOVED.git-id | 1 - 19 files changed, 2142 insertions(+), 19 deletions(-) create mode 100644 src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs delete mode 100644 src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/Car.bmp delete mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/Car.bmp.REMOVED.git-id create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/neg_height.bmp delete mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/neg_height.bmp.REMOVED.git-id create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Calliphora.jpg delete mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Calliphora.jpg.REMOVED.git-id create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Floorplan.jpeg delete mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Floorplan.jpeg.REMOVED.git-id create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/cmyk.jpg delete mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/cmyk.jpg.REMOVED.git-id create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg delete mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg.REMOVED.git-id create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Png/blur.png delete mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Png/splash.png delete mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Png/splash.png.REMOVED.git-id diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs b/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs new file mode 100644 index 0000000000..cee4b682b4 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs @@ -0,0 +1,2100 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + using System.Threading.Tasks; + + /// + /// Performs the jpeg decoding operation. + /// + internal class JpegDecoderCore + { + /// + /// The maximum (inclusive) number of bits in a Huffman code. + /// + private const int MaxCodeLength = 16; + + /// + /// The maximum (inclusive) number of codes in a Huffman tree. + /// + private const int MaxNCodes = 256; + + /// + /// The log-2 size of the Huffman decoder's look-up table. + /// + private const int LutSize = 8; + + /// + /// The maximum number of color components + /// + private const int MaxComponents = 4; + + private const int maxTc = 1; + + private const int maxTh = 3; + + private const int maxTq = 3; + + private const int dcTable = 0; + + private const int acTable = 1; + + /// + /// Unzig maps from the zigzag ordering to the natural ordering. For example, + /// unzig[3] is the column and row of the fourth element in zigzag order. The + /// value is 16, which means first column (16%8 == 0) and third row (16/8 == 2). + /// + private static readonly int[] Unzig = + { + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, + 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, + 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, + 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, + }; + + /// + /// The component array + /// + private readonly Component[] componentArray; + + /// + /// Saved state between progressive-mode scans. + /// + private readonly Block[][] progCoeffs; + + /// + /// The huffman trees + /// + private readonly Huffman[,] huffmanTrees; + + /// + /// Quantization tables, in zigzag order. + /// + private readonly Block[] quantizationTables; + + /// + /// The byte buffer. + /// + private readonly Bytes bytes; + + /// + /// The image width + /// + private int imageWidth; + + /// + /// The image height + /// + private int imageHeight; + + /// + /// The number of color components within the image. + /// + private int componentCount; + + /// + /// A grayscale image to decode to. + /// + private GrayImage grayImage; + + /// + /// The full color image to decode to. + /// + private YCbCrImage ycbcrImage; + + /// + /// The input stream. + /// + private Stream inputStream; + + /// + /// Holds the unprocessed bits that have been taken from the byte-stream. + /// + private Bits bits; + + /// + /// The array of keyline pixels in a CMYK image + /// + private byte[] blackPixels; + + /// + /// The width in bytes or a single row of keyline pixels in a CMYK image + /// + private int blackStride; + + /// + /// The restart interval + /// + private int restartInterval; + + /// + /// Whether the image is interlaced (progressive) + /// + private bool isProgressive; + + /// + /// Whether the image has a JFIF header + /// + private bool isJfif; + + private bool adobeTransformValid; + + private byte adobeTransform; + + /// + /// End-of-Band run, specified in section G.1.2.2. + /// + private ushort eobRun; + + /// + /// A temporary buffer for holding pixels + /// + private byte[] temp; + + /// + /// The horizontal resolution. Calculated if the image has a JFIF header. + /// + private short horizontalResolution; + + /// + /// The vertical resolution. Calculated if the image has a JFIF header. + /// + private short verticalResolution; + + /// + /// Initializes a new instance of the class. + /// + public JpegDecoderCore() + { + this.huffmanTrees = new Huffman[maxTc + 1, maxTh + 1]; + this.quantizationTables = new Block[maxTq + 1]; + this.temp = new byte[2 * Block.BlockSize]; + this.componentArray = new Component[MaxComponents]; + this.progCoeffs = new Block[MaxComponents][]; + this.bits = new Bits(); + this.bytes = new Bytes(); + + for (int i = 0; i < maxTc + 1; i++) + { + for (int j = 0; j < maxTh + 1; j++) + { + this.huffmanTrees[i, j] = new Huffman(LutSize, MaxNCodes, MaxCodeLength); + } + } + + for (int i = 0; i < this.quantizationTables.Length; i++) + { + this.quantizationTables[i] = new Block(); + } + + for (int i = 0; i < this.componentArray.Length; i++) + { + this.componentArray[i] = new Component(); + } + } + + /// + /// Decodes the image from the specified this._stream and sets + /// the data to image. + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The stream, where the image should be + /// decoded from. Cannot be null (Nothing in Visual Basic). + /// The image, where the data should be set to. + /// Cannot be null (Nothing in Visual Basic). + /// Whether to decode metadata only. + public void Decode(Image image, Stream stream, bool configOnly) + where TColor : struct, IPackedPixel + where TPacked : struct + { + this.inputStream = stream; + + // Check for the Start Of Image marker. + this.ReadFull(this.temp, 0, 2); + if (this.temp[0] != JpegConstants.Markers.XFF || this.temp[1] != JpegConstants.Markers.SOI) + { + throw new ImageFormatException("Missing SOI marker."); + } + + // Process the remaining segments until the End Of Image marker. + while (true) + { + this.ReadFull(this.temp, 0, 2); + while (this.temp[0] != 0xff) + { + // Strictly speaking, this is a format error. However, libjpeg is + // liberal in what it accepts. As of version 9, next_marker in + // jdmarker.c treats this as a warning (JWRN_EXTRANEOUS_DATA) and + // continues to decode the stream. Even before next_marker sees + // extraneous data, jpeg_fill_bit_buffer in jdhuff.c reads as many + // bytes as it can, possibly past the end of a scan's data. It + // effectively puts back any markers that it overscanned (e.g. an + // "\xff\xd9" EOI marker), but it does not put back non-marker data, + // and thus it can silently ignore a small number of extraneous + // non-marker bytes before next_marker has a chance to see them (and + // print a warning). + // We are therefore also liberal in what we accept. Extraneous data + // is silently ignore + // This is similar to, but not exactly the same as, the restart + // mechanism within a scan (the RST[0-7] markers). + // Note that extraneous 0xff bytes in e.g. SOS data are escaped as + // "\xff\x00", and so are detected a little further down below. + this.temp[0] = this.temp[1]; + this.temp[1] = this.ReadByte(); + } + + byte marker = this.temp[1]; + if (marker == 0) + { + // Treat "\xff\x00" as extraneous data. + continue; + } + + while (marker == 0xff) + { + // Section B.1.1.2 says, "Any marker may optionally be preceded by any + // number of fill bytes, which are bytes assigned code X'FF'". + marker = this.ReadByte(); + } + + // End Of Image. + if (marker == JpegConstants.Markers.EOI) + { + break; + } + + if (JpegConstants.Markers.RST0 <= marker && marker <= JpegConstants.Markers.RST7) + { + // Figures B.2 and B.16 of the specification suggest that restart markers should + // only occur between Entropy Coded Segments and not after the final ECS. + // However, some encoders may generate incorrect JPEGs with a final restart + // marker. That restart marker will be seen here instead of inside the ProcessSOS + // method, and is ignored as a harmless error. Restart markers have no extra data, + // so we check for this before we read the 16-bit length of the segment. + continue; + } + + // Read the 16-bit length of the segment. The value includes the 2 bytes for the + // length itself, so we subtract 2 to get the number of remaining bytes. + this.ReadFull(this.temp, 0, 2); + int remaining = ((int)this.temp[0] << 8) + (int)this.temp[1] - 2; + if (remaining < 0) + { + throw new ImageFormatException("Short segment length."); + } + + switch (marker) + { + case JpegConstants.Markers.SOF0: + case JpegConstants.Markers.SOF1: + case JpegConstants.Markers.SOF2: + this.isProgressive = marker == JpegConstants.Markers.SOF2; + this.ProcessSOF(remaining); + if (configOnly && this.isJfif) + { + return; + } + + break; + case JpegConstants.Markers.DHT: + if (configOnly) + { + this.Skip(remaining); + } + else + { + this.ProcessDht(remaining); + } + + break; + case JpegConstants.Markers.DQT: + if (configOnly) + { + this.Skip(remaining); + } + else this.ProcessDqt(remaining); + break; + case JpegConstants.Markers.SOS: + if (configOnly) + { + return; + } + + this.ProcessStartOfScan(remaining); + break; + case JpegConstants.Markers.DRI: + if (configOnly) + { + this.Skip(remaining); + } + else + { + this.ProcessDri(remaining); + } + + break; + case JpegConstants.Markers.APP0: + this.ProcessApp0Marker(remaining); + break; + case JpegConstants.Markers.APP1: + this.ProcessApp1Marker(remaining, image); + break; + case JpegConstants.Markers.APP14: + this.ProcessApp14Marker(remaining); + break; + default: + if (JpegConstants.Markers.APP0 <= marker && marker <= JpegConstants.Markers.APP15 || marker == JpegConstants.Markers.COM) + { + this.Skip(remaining); + } + else if (marker < JpegConstants.Markers.SOF0) + { + // See Table B.1 "Marker code assignments". + throw new ImageFormatException("Unknown marker"); + } + else + { + throw new ImageFormatException("Unknown marker"); + } + + break; + } + } + + if (this.grayImage != null) + { + this.ConvertFromGrayScale(this.imageWidth, this.imageHeight, image); + } + else if (this.ycbcrImage != null) + { + if (this.componentCount == 4) + { + this.ConvertFromCmyk(this.imageWidth, this.imageHeight, image); + return; + } + + if (this.componentCount == 3) + { + if (this.IsRGB()) + { + this.ConvertFromRGB(this.imageWidth, this.imageHeight, image); + return; + } + + this.ConvertFromYCbCr(this.imageWidth, this.imageHeight, image); + return; + } + + throw new ImageFormatException("JpegDecoder only supports RGB, CMYK and Grayscale color spaces."); + } + else + { + throw new ImageFormatException("Missing SOS marker."); + } + } + + /// + /// Reads bytes from the byte buffer to ensure that bits.UnreadBits is at + /// least n. For best performance (avoiding function calls inside hot loops), + /// the caller is the one responsible for first checking that bits.UnreadBits < n. + /// + /// The number of bits to ensure. + private void EnsureNBits(int n) + { + while (true) + { + byte c = this.ReadByteStuffedByte(); + this.bits.Accumulator = (this.bits.Accumulator << 8) | c; + this.bits.UnreadBits += 8; + if (this.bits.Mask == 0) + { + this.bits.Mask = 1 << 7; + } + else + { + this.bits.Mask <<= 8; + } + + if (this.bits.UnreadBits >= n) + { + break; + } + } + } + + /// + /// The composition of RECEIVE and EXTEND, specified in section F.2.2.1. + /// + /// The byte + /// The + private int ReceiveExtend(byte t) + { + if (this.bits.UnreadBits < t) + { + this.EnsureNBits(t); + } + + this.bits.UnreadBits -= t; + this.bits.Mask >>= t; + int s = 1 << t; + int x = (int)((this.bits.Accumulator >> this.bits.UnreadBits) & (s - 1)); + + if (x < (s >> 1)) + { + x += ((-1) << t) + 1; + } + + return x; + } + + /// + /// Processes a Define Huffman Table marker, and initializes a huffman + /// struct from its contents. Specified in section B.2.4.2. + /// + /// + private void ProcessDht(int n) + { + while (n > 0) + { + if (n < 17) + { + throw new ImageFormatException("DHT has wrong length"); + } + + this.ReadFull(this.temp, 0, 17); + + int tc = this.temp[0] >> 4; + if (tc > maxTc) + { + throw new ImageFormatException("Bad Tc value"); + } + + int th = this.temp[0] & 0x0f; + if (th > maxTh || (!this.isProgressive && (th > 1))) + { + throw new ImageFormatException("Bad Th value"); + } + + Huffman huffman = this.huffmanTrees[tc, th]; + + // Read nCodes and huffman.Valuess (and derive h.Length). + // nCodes[i] is the number of codes with code length i. + // h.Length is the total number of codes. + huffman.Length = 0; + + int[] ncodes = new int[MaxCodeLength]; + for (int i = 0; i < ncodes.Length; i++) + { + ncodes[i] = this.temp[i + 1]; + huffman.Length += ncodes[i]; + } + + if (huffman.Length == 0) + { + throw new ImageFormatException("Huffman table has zero length"); + } + + if (huffman.Length > MaxNCodes) + { + throw new ImageFormatException("Huffman table has excessive length"); + } + + n -= huffman.Length + 17; + if (n < 0) + { + throw new ImageFormatException("DHT has wrong length"); + } + + this.ReadFull(huffman.Values, 0, huffman.Length); + + // Derive the look-up table. + for (int i = 0; i < huffman.Lut.Length; i++) + { + huffman.Lut[i] = 0; + } + + uint x = 0, code = 0; + + for (int i = 0; i < LutSize; i++) + { + code <<= 1; + + for (int j = 0; j < ncodes[i]; j++) + { + // The codeLength is 1+i, so shift code by 8-(1+i) to + // calculate the high bits for every 8-bit sequence + // whose codeLength's high bits matches code. + // The high 8 bits of lutValue are the encoded value. + // The low 8 bits are 1 plus the codeLength. + byte base2 = (byte)(code << (7 - i)); + ushort lutValue = (ushort)((huffman.Values[x] << 8) | (2 + i)); + + for (int k = 0; k < 1 << (7 - i); k++) + { + huffman.Lut[base2 | k] = lutValue; + } + + code++; + x++; + } + } + + // Derive minCodes, maxCodes, and indices. + int c = 0, index = 0; + for (int i = 0; i < ncodes.Length; i++) + { + int nc = ncodes[i]; + if (nc == 0) + { + huffman.MinCodes[i] = -1; + huffman.MaxCodes[i] = -1; + huffman.Indices[i] = -1; + } + else + { + huffman.MinCodes[i] = c; + huffman.MaxCodes[i] = c + nc - 1; + huffman.Indices[i] = index; + c += nc; + index += nc; + } + + c <<= 1; + } + } + } + + /// + /// Returns the next Huffman-coded value from the bit-stream, + /// decoded according to the given value. + /// + /// The huffman value + /// The + private byte DecodeHuffman(Huffman huffman) + { + if (huffman.Length == 0) + { + throw new ImageFormatException("Uninitialized Huffman table"); + } + + if (this.bits.UnreadBits < 8) + { + try + { + this.EnsureNBits(8); + + ushort v = huffman.Lut[(this.bits.Accumulator >> (this.bits.UnreadBits - LutSize)) & 0xff]; + + if (v != 0) + { + byte n = (byte)((v & 0xff) - 1); + this.bits.UnreadBits -= n; + this.bits.Mask >>= n; + return (byte)(v >> 8); + } + } + catch (MissingFF00Exception) + { + if (this.bytes.UnreadableBytes != 0) + { + this.UnreadByteStuffedByte(); + } + } + catch (ShortHuffmanDataException) + { + if (this.bytes.UnreadableBytes != 0) + { + this.UnreadByteStuffedByte(); + } + } + } + + int code = 0; + for (int i = 0; i < MaxCodeLength; i++) + { + if (this.bits.UnreadBits == 0) + { + this.EnsureNBits(1); + } + + if ((this.bits.Accumulator & this.bits.Mask) != 0) + { + code |= 1; + } + + this.bits.UnreadBits--; + this.bits.Mask >>= 1; + + if (code <= huffman.MaxCodes[i]) + { + return huffman.Values[huffman.Indices[i] + code - huffman.MinCodes[i]]; + } + + code <<= 1; + } + + throw new ImageFormatException("Bad Huffman code"); + } + + /// + /// Decodes a single bit + /// + /// The + private bool DecodeBit() + { + if (this.bits.UnreadBits == 0) + { + this.EnsureNBits(1); + } + + bool ret = (this.bits.Accumulator & this.bits.Mask) != 0; + this.bits.UnreadBits--; + this.bits.Mask >>= 1; + return ret; + } + + /// + /// Decodes the given number of bits + /// + /// The number of bits to decode. + /// The + private uint DecodeBits(int count) + { + if (this.bits.UnreadBits < count) + { + this.EnsureNBits(count); + } + + uint ret = this.bits.Accumulator >> (this.bits.UnreadBits - count); + ret = (uint)(ret & ((1 << count) - 1)); + this.bits.UnreadBits -= count; + this.bits.Mask >>= count; + return ret; + } + + /// + /// Fills up the bytes buffer from the underlying stream. + /// It should only be called when there are no unread bytes in bytes. + /// + private void Fill() + { + if (this.bytes.I != this.bytes.J) + { + throw new ImageFormatException("Fill called when unread bytes exist."); + } + + // Move the last 2 bytes to the start of the buffer, in case we need + // to call UnreadByteStuffedByte. + if (this.bytes.J > 2) + { + this.bytes.Buffer[0] = this.bytes.Buffer[this.bytes.J - 2]; + this.bytes.Buffer[1] = this.bytes.Buffer[this.bytes.J - 1]; + this.bytes.I = 2; + this.bytes.J = 2; + } + + // Fill in the rest of the buffer. + int n = this.inputStream.Read(this.bytes.Buffer, this.bytes.J, this.bytes.Buffer.Length - this.bytes.J); + if (n == 0) + { + throw new EOFException(); + } + + this.bytes.J += n; + } + + /// + /// Undoes the most recent ReadByteStuffedByte call, + /// giving a byte of data back from bits to bytes. The Huffman look-up table + /// requires at least 8 bits for look-up, which means that Huffman decoding can + /// sometimes overshoot and read one or two too many bytes. Two-byte overshoot + /// can happen when expecting to read a 0xff 0x00 byte-stuffed byte. + /// + private void UnreadByteStuffedByte() + { + this.bytes.I -= this.bytes.UnreadableBytes; + this.bytes.UnreadableBytes = 0; + if (this.bits.UnreadBits >= 8) + { + this.bits.Accumulator >>= 8; + this.bits.UnreadBits -= 8; + this.bits.Mask >>= 8; + } + } + + /// + /// Returns the next byte, whether buffered or not buffered. It does not care about byte stuffing. + /// + /// The + private byte ReadByte() + { + while (this.bytes.I == this.bytes.J) + { + this.Fill(); + } + + byte x = this.bytes.Buffer[this.bytes.I]; + this.bytes.I++; + this.bytes.UnreadableBytes = 0; + return x; + } + + /// + /// ReadByteStuffedByte is like ReadByte but is for byte-stuffed Huffman data. + /// + /// The + private byte ReadByteStuffedByte() + { + byte x; + + // Take the fast path if bytes.buf contains at least two bytes. + if (this.bytes.I + 2 <= this.bytes.J) + { + x = this.bytes.Buffer[this.bytes.I]; + this.bytes.I++; + this.bytes.UnreadableBytes = 1; + if (x != JpegConstants.Markers.XFF) + { + return x; + } + + if (this.bytes.Buffer[this.bytes.I] != 0x00) + { + throw new MissingFF00Exception(); + } + + this.bytes.I++; + this.bytes.UnreadableBytes = 2; + return JpegConstants.Markers.XFF; + } + + this.bytes.UnreadableBytes = 0; + + x = this.ReadByte(); + this.bytes.UnreadableBytes = 1; + if (x != JpegConstants.Markers.XFF) + { + return x; + } + + x = this.ReadByte(); + this.bytes.UnreadableBytes = 2; + if (x != 0x00) + { + throw new MissingFF00Exception(); + } + + return JpegConstants.Markers.XFF; + } + + /// + /// Reads exactly length bytes into data. It does not care about byte stuffing. + /// + /// The data to write to. + /// The offset in the source buffer + /// The number of bytes to read + private void ReadFull(byte[] data, int offset, int length) + { + // Unread the overshot bytes, if any. + if (this.bytes.UnreadableBytes != 0) + { + if (this.bits.UnreadBits >= 8) + { + this.UnreadByteStuffedByte(); + } + + this.bytes.UnreadableBytes = 0; + } + + while (length > 0) + { + if (this.bytes.J - this.bytes.I >= length) + { + Array.Copy(this.bytes.Buffer, this.bytes.I, data, offset, length); + this.bytes.I += length; + length -= length; + } + else + { + Array.Copy(this.bytes.Buffer, this.bytes.I, data, offset, this.bytes.J - this.bytes.I); + offset += this.bytes.J - this.bytes.I; + length -= this.bytes.J - this.bytes.I; + this.bytes.I += this.bytes.J - this.bytes.I; + + this.Fill(); + } + } + } + + /// + /// Skips the next n bytes. + /// + /// The number of bytes to ignore. + private void Skip(int count) + { + // Unread the overshot bytes, if any. + if (this.bytes.UnreadableBytes != 0) + { + if (this.bits.UnreadBits >= 8) + { + this.UnreadByteStuffedByte(); + } + + this.bytes.UnreadableBytes = 0; + } + + while (true) + { + int m = this.bytes.J - this.bytes.I; + if (m > count) + { + m = count; + } + + this.bytes.I += m; + count -= m; + if (count == 0) + { + break; + } + + this.Fill(); + } + } + + // Specified in section B.2.2. + private void ProcessSOF(int n) + { + if (this.componentCount != 0) + { + throw new ImageFormatException("multiple SOF markers"); + } + + switch (n) + { + case 6 + (3 * 1): // Grayscale image. + this.componentCount = 1; + break; + case 6 + (3 * 3): // YCbCr or RGB image. + this.componentCount = 3; + break; + case 6 + (3 * 4): // YCbCrK or CMYK image. + this.componentCount = 4; + break; + default: + throw new ImageFormatException("Incorrect number of components"); + } + + this.ReadFull(this.temp, 0, n); + + // We only support 8-bit precision. + if (this.temp[0] != 8) + { + throw new ImageFormatException("Only 8-Bit precision supported."); + } + + this.imageHeight = (this.temp[1] << 8) + this.temp[2]; + this.imageWidth = (this.temp[3] << 8) + this.temp[4]; + if (this.temp[5] != this.componentCount) + { + throw new ImageFormatException("SOF has wrong length"); + } + + for (int i = 0; i < this.componentCount; i++) + { + this.componentArray[i].Identifier = this.temp[6 + (3 * i)]; + + // Section B.2.2 states that "the value of C_i shall be different from + // the values of C_1 through C_(i-1)". + for (int j = 0; j < i; j++) + { + if (this.componentArray[i].Identifier == this.componentArray[j].Identifier) + { + throw new ImageFormatException("Repeated component identifier"); + } + } + + this.componentArray[i].Selector = this.temp[8 + (3 * i)]; + if (this.componentArray[i].Selector > maxTq) + { + throw new ImageFormatException("Bad Tq value"); + } + + byte hv = this.temp[7 + (3 * i)]; + int h = hv >> 4; + int v = hv & 0x0f; + if (h < 1 || h > 4 || v < 1 || v > 4) + { + throw new ImageFormatException("Unsupported Luma/chroma subsampling ratio"); + } + + if (h == 3 || v == 3) + { + throw new ImageFormatException("Lnsupported subsampling ratio"); + } + + switch (this.componentCount) + { + case 1: + + // If a JPEG image has only one component, section A.2 says "this data + // is non-interleaved by definition" and section A.2.2 says "[in this + // case...] the order of data units within a scan shall be left-to-right + // and top-to-bottom... regardless of the values of H_1 and V_1". Section + // 4.8.2 also says "[for non-interleaved data], the MCU is defined to be + // one data unit". Similarly, section A.1.1 explains that it is the ratio + // of H_i to max_j(H_j) that matters, and similarly for V. For grayscale + // images, H_1 is the maximum H_j for all components j, so that ratio is + // always 1. The component's (h, v) is effectively always (1, 1): even if + // the nominal (h, v) is (2, 1), a 20x5 image is encoded in three 8x8 + // MCUs, not two 16x8 MCUs. + h = 1; + v = 1; + break; + + case 3: + + // For YCbCr images, we only support 4:4:4, 4:4:0, 4:2:2, 4:2:0, + // 4:1:1 or 4:1:0 chroma subsampling ratios. This implies that the + // (h, v) values for the Y component are either (1, 1), (1, 2), + // (2, 1), (2, 2), (4, 1) or (4, 2), and the Y component's values + // must be a multiple of the Cb and Cr component's values. We also + // assume that the two chroma components have the same subsampling + // ratio. + switch (i) + { + case 0: + { + // Y. + // We have already verified, above, that h and v are both + // either 1, 2 or 4, so invalid (h, v) combinations are those + // with v == 4. + if (v == 4) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } + + break; + } + + case 1: + { + // Cb. + if (this.componentArray[0].HorizontalFactor % h != 0 || this.componentArray[0].VerticalFactor % v != 0) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } + + break; + } + + case 2: + { + // Cr. + if (this.componentArray[1].HorizontalFactor != h || this.componentArray[1].VerticalFactor != v) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } + + break; + } + } + + break; + + case 4: + + // For 4-component images (either CMYK or YCbCrK), we only support two + // hv vectors: [0x11 0x11 0x11 0x11] and [0x22 0x11 0x11 0x22]. + // Theoretically, 4-component JPEG images could mix and match hv values + // but in practice, those two combinations are the only ones in use, + // and it simplifies the applyBlack code below if we can assume that: + // - for CMYK, the C and K channels have full samples, and if the M + // and Y channels subsample, they subsample both horizontally and + // vertically. + // - for YCbCrK, the Y and K channels have full samples. + switch (i) + { + case 0: + if (hv != 0x11 && hv != 0x22) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } + + break; + case 1: + case 2: + if (hv != 0x11) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } + + break; + case 3: + if (this.componentArray[0].HorizontalFactor != h || this.componentArray[0].VerticalFactor != v) + { + throw new ImageFormatException("Unsupported subsampling ratio"); + } + + break; + } + + break; + } + + this.componentArray[i].HorizontalFactor = h; + this.componentArray[i].VerticalFactor = v; + } + } + + // Specified in section B.2.4.1. + private void ProcessDqt(int n) + { + while (n > 0) + { + bool done = false; + + n--; + byte x = this.ReadByte(); + byte tq = (byte)(x & 0x0f); + if (tq > maxTq) + { + throw new ImageFormatException("Bad Tq value"); + } + + switch (x >> 4) + { + case 0: + if (n < Block.BlockSize) + { + done = true; + break; + } + + n -= Block.BlockSize; + this.ReadFull(this.temp, 0, Block.BlockSize); + + for (int i = 0; i < Block.BlockSize; i++) + { + this.quantizationTables[tq][i] = this.temp[i]; + } + + break; + case 1: + if (n < 2 * Block.BlockSize) + { + done = true; + break; + } + + n -= 2 * Block.BlockSize; + this.ReadFull(this.temp, 0, 2 * Block.BlockSize); + + for (int i = 0; i < Block.BlockSize; i++) + { + this.quantizationTables[tq][i] = (this.temp[2 * i] << 8) | this.temp[(2 * i) + 1]; + } + + break; + default: + throw new ImageFormatException("Bad Pq value"); + } + + if (done) + { + break; + } + } + + if (n != 0) + { + throw new ImageFormatException("DQT has wrong length"); + } + } + + // Specified in section B.2.4.4. + private void ProcessDri(int n) + { + if (n != 2) + { + throw new ImageFormatException("DRI has wrong length"); + } + + this.ReadFull(this.temp, 0, 2); + this.restartInterval = ((int)this.temp[0] << 8) + (int)this.temp[1]; + } + + private void ProcessApp0Marker(int n) + { + if (n < 5) + { + this.Skip(n); + return; + } + + this.ReadFull(this.temp, 0, 13); + n -= 13; + + // TODO: We should be using constants for this. + this.isJfif = this.temp[0] == 'J' && + this.temp[1] == 'F' && + this.temp[2] == 'I' && + this.temp[3] == 'F' && + this.temp[4] == '\x00'; + + if (this.isJfif) + { + this.horizontalResolution = (short)(this.temp[9] + (this.temp[10] << 8)); + this.verticalResolution = (short)(this.temp[11] + (this.temp[12] << 8)); + } + + if (n > 0) + { + this.Skip(n); + } + } + + /// + /// Processes the App1 marker retrieving any stored metadata + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The position in the stream. + /// The image. + private void ProcessApp1Marker(int n, Image image) + where TColor : struct, IPackedPixel + where TPacked : struct + { + if (n < 6) + { + this.Skip(n); + return; + } + + byte[] profile = new byte[n]; + this.ReadFull(profile, 0, n); + + if (profile[0] == 'E' && + profile[1] == 'x' && + profile[2] == 'i' && + profile[3] == 'f' && + profile[4] == '\0' && + profile[5] == '\0') + { + image.ExifProfile = new ExifProfile(profile); + } + } + + private void ProcessApp14Marker(int n) + { + if (n < 12) + { + this.Skip(n); + return; + } + + this.ReadFull(this.temp, 0, 12); + n -= 12; + + if (this.temp[0] == 'A' && + this.temp[1] == 'd' && + this.temp[2] == 'o' && + this.temp[3] == 'b' && + this.temp[4] == 'e') + { + this.adobeTransformValid = true; + this.adobeTransform = this.temp[11]; + } + + if (n > 0) + { + this.Skip(n); + } + } + + /// + /// Converts the image from the original Cmyk image pixels. + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The image width. + /// The image height. + /// The image. + private void ConvertFromCmyk(int width, int height, Image image) + where TColor : struct, IPackedPixel + where TPacked : struct + { + if (!this.adobeTransformValid) + { + throw new ImageFormatException("Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); + } + + // If the 4-component JPEG image isn't explicitly marked as "Unknown (RGB + // or CMYK)" as per http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe + if (this.adobeTransform != JpegConstants.Adobe.ColorTransformUnknown) + { + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; + + TColor[] pixels = new TColor[width * height]; + + // Convert the YCbCr part of the YCbCrK to RGB, invert the RGB to get + // CMY, and patch in the original K. The RGB to CMY inversion cancels + // out the 'Adobe inversion' described in the applyBlack doc comment + // above, so in practice, only the fourth channel (black) is inverted. + Parallel.For( + 0, + height, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) + { + byte yy = this.ycbcrImage.YChannel[yo + x]; + byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; + + int index = (y * width) + x; + + // Implicit casting FTW + Color color = new YCbCr(yy, cb, cr); + int keyline = 255 - this.blackPixels[y * this.blackStride + x]; + Color final = new Cmyk(color.R / 255F, color.G / 255F, color.B / 255F, keyline / 255F); + + TColor packed = default(TColor); + packed.PackFromVector4(final.ToVector4()); + pixels[index] = packed; + } + }); + + image.SetPixels(width, height, pixels); + } + } + + /// + /// Converts the image from the original grayscale image pixels. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image width. + /// The image height. + /// The image. + private void ConvertFromGrayScale(int width, int height, Image image) + where TColor : struct, IPackedPixel + where TPacked : struct + { + TColor[] pixels = new TColor[width * height]; + + Parallel.For( + 0, + height, + Bootstrapper.Instance.ParallelOptions, + y => + { + int yoff = this.grayImage.GetRowOffset(y); + for (int x = 0; x < width; x++) + { + int offset = (y * width) + x; + byte rgb = this.grayImage.Pixels[yoff + x]; + + TColor packed = default(TColor); + packed.PackFromVector4(new Color(rgb, rgb, rgb).ToVector4()); + pixels[offset] = packed; + } + }); + + image.SetPixels(width, height, pixels); + this.AssignResolution(image); + } + + /// + /// Converts the image from the original YCbCr image pixels. + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The image width. + /// The image height. + /// The image. + private void ConvertFromYCbCr(int width, int height, Image image) + where TColor : struct, IPackedPixel + where TPacked : struct + { + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; + + TColor[] pixels = new TColor[width * height]; + + Parallel.For( + 0, + height, + Bootstrapper.Instance.ParallelOptions, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) + { + byte yy = this.ycbcrImage.YChannel[yo + x]; + byte cb = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel[co + (x / scale)]; + + int index = (y * width) + x; + + // Implicit casting FTW + Color color = new YCbCr(yy, cb, cr); + TColor packed = default(TColor); + packed.PackFromVector4(color.ToVector4()); + pixels[index] = packed; + } + }); + + image.SetPixels(width, height, pixels); + this.AssignResolution(image); + } + + /// + /// Converts the image from the original RBG image pixels. + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The image width. + /// The height. + /// The image. + private void ConvertFromRGB(int width, int height, Image image) + where TColor : struct, IPackedPixel + where TPacked : struct + { + int scale = this.componentArray[0].HorizontalFactor / this.componentArray[1].HorizontalFactor; + TColor[] pixels = new TColor[width * height]; + + Parallel.For( + 0, + height, + Bootstrapper.Instance.ParallelOptions, + y => + { + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < width; x++) + { + byte red = this.ycbcrImage.YChannel[yo + x]; + byte green = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte blue = this.ycbcrImage.CrChannel[co + (x / scale)]; + + int index = (y * width) + x; + TColor packed = default(TColor); + packed.PackFromVector4(new Color(red, green, blue).ToVector4()); + + pixels[index] = packed; + } + }); + + image.SetPixels(width, height, pixels); + this.AssignResolution(image); + } + + /// + /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header. + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// The image to assign the resolution to. + private void AssignResolution(Image image) + where TColor : struct, IPackedPixel + where TPacked : struct + { + if (this.isJfif && this.horizontalResolution > 0 && this.verticalResolution > 0) + { + image.HorizontalResolution = this.horizontalResolution; + image.VerticalResolution = this.verticalResolution; + } + } + + /// + /// Processes the SOS (Start of scan marker). + /// + /// + /// TODO: This also needs some significant refactoring to follow a more OO format. + /// + /// + /// The first byte of the current image marker. + /// + /// + /// Missing SOF Marker + /// SOS has wrong length + /// + private void ProcessStartOfScan(int n) + { + if (this.componentCount == 0) + { + throw new ImageFormatException("Missing SOF marker"); + } + + if (n < 6 || 4 + (2 * this.componentCount) < n || n % 2 != 0) + { + throw new ImageFormatException("SOS has wrong length"); + } + + this.ReadFull(this.temp, 0, n); + byte lnComp = this.temp[0]; + + if (n != 4 + (2 * lnComp)) + { + throw new ImageFormatException("SOS length inconsistent with number of components"); + } + + Scan[] scan = new Scan[MaxComponents]; + int totalHv = 0; + + for (int i = 0; i < lnComp; i++) + { + // Component selector. + int cs = this.temp[1 + (2 * i)]; + int compIndex = -1; + for (int j = 0; j < this.componentCount; j++) + { + Component compv = this.componentArray[j]; + if (cs == compv.Identifier) + { + compIndex = j; + } + } + + if (compIndex < 0) + { + throw new ImageFormatException("Unknown component selector"); + } + + scan[i].Index = (byte)compIndex; + + // Section B.2.3 states that "the value of Cs_j shall be different from + // the values of Cs_1 through Cs_(j-1)". Since we have previously + // verified that a frame's component identifiers (C_i values in section + // B.2.2) are unique, it suffices to check that the implicit indexes + // into comp are unique. + for (int j = 0; j < i; j++) + { + if (scan[i].Index == scan[j].Index) + { + throw new ImageFormatException("Repeated component selector"); + } + } + + totalHv += this.componentArray[compIndex].HorizontalFactor + * this.componentArray[compIndex].VerticalFactor; + + scan[i].DcTableSelector = (byte)(this.temp[2 + (2 * i)] >> 4); + if (scan[i].DcTableSelector > maxTh) + { + throw new ImageFormatException("Bad DC table selector value"); + } + + scan[i].AcTableSelector = (byte)(this.temp[2 + (2 * i)] & 0x0f); + if (scan[i].AcTableSelector > maxTh) + { + throw new ImageFormatException("Bad AC table selector value"); + } + } + + // Section B.2.3 states that if there is more than one component then the + // total H*V values in a scan must be <= 10. + if (this.componentCount > 1 && totalHv > 10) + { + throw new ImageFormatException("Total sampling factors too large."); + } + + // zigStart and zigEnd are the spectral selection bounds. + // ah and al are the successive approximation high and low values. + // The spec calls these values Ss, Se, Ah and Al. + // For progressive JPEGs, these are the two more-or-less independent + // aspects of progression. Spectral selection progression is when not + // all of a block's 64 DCT coefficients are transmitted in one pass. + // For example, three passes could transmit coefficient 0 (the DC + // component), coefficients 1-5, and coefficients 6-63, in zig-zag + // order. Successive approximation is when not all of the bits of a + // band of coefficients are transmitted in one pass. For example, + // three passes could transmit the 6 most significant bits, followed + // by the second-least significant bit, followed by the least + // significant bit. + // For baseline JPEGs, these parameters are hard-coded to 0/63/0/0. + int zigStart = 0; + int zigEnd = Block.BlockSize - 1; + int ah = 0; + int al = 0; + + if (this.isProgressive) + { + zigStart = (int)this.temp[1 + (2 * lnComp)]; + zigEnd = (int)this.temp[2 + (2 * lnComp)]; + ah = (int)(this.temp[3 + (2 * lnComp)] >> 4); + al = (int)(this.temp[3 + (2 * lnComp)] & 0x0f); + + if ((zigStart == 0 && zigEnd != 0) || zigStart > zigEnd || Block.BlockSize <= zigEnd) + { + throw new ImageFormatException("Bad spectral selection bounds"); + } + + if (zigStart != 0 && lnComp != 1) + { + throw new ImageFormatException("Progressive AC coefficients for more than one component"); + } + + if (ah != 0 && ah != al + 1) + { + throw new ImageFormatException("Bad successive approximation values"); + } + } + + // mxx and myy are the number of MCUs (Minimum Coded Units) in the image. + int h0 = this.componentArray[0].HorizontalFactor; + int v0 = this.componentArray[0].VerticalFactor; + int mxx = (this.imageWidth + (8 * h0) - 1) / (8 * h0); + int myy = (this.imageHeight + (8 * v0) - 1) / (8 * v0); + + if (this.grayImage == null && this.ycbcrImage == null) + { + this.MakeImage(mxx, myy); + } + + if (this.isProgressive) + { + for (int i = 0; i < lnComp; i++) + { + int compIndex = scan[i].Index; + if (this.progCoeffs[compIndex] == null) + { + this.progCoeffs[compIndex] = new Block[mxx * myy * this.componentArray[compIndex].HorizontalFactor * this.componentArray[compIndex].VerticalFactor]; + + for (int j = 0; j < this.progCoeffs[compIndex].Length; j++) + { + this.progCoeffs[compIndex][j] = new Block(); + } + } + } + } + + this.bits = new Bits(); + + int mcu = 0; + byte expectedRst = JpegConstants.Markers.RST0; + + // b is the decoded coefficients, in natural (not zig-zag) order. + Block b; + int[] dc = new int[MaxComponents]; + + // bx and by are the location of the current block, in units of 8x8 + // blocks: the third block in the first row has (bx, by) = (2, 0). + int bx, by, blockCount = 0; + + for (int my = 0; my < myy; my++) + { + for (int mx = 0; mx < mxx; mx++) + { + for (int i = 0; i < lnComp; i++) + { + int compIndex = scan[i].Index; + int hi = this.componentArray[compIndex].HorizontalFactor; + int vi = this.componentArray[compIndex].VerticalFactor; + Block qt = this.quantizationTables[this.componentArray[compIndex].Selector]; + + for (int j = 0; j < hi * vi; j++) + { + // The blocks are traversed one MCU at a time. For 4:2:0 chroma + // subsampling, there are four Y 8x8 blocks in every 16x16 MCU. + // For a baseline 32x16 pixel image, the Y blocks visiting order is: + // 0 1 4 5 + // 2 3 6 7 + // For progressive images, the interleaved scans (those with nComp > 1) + // are traversed as above, but non-interleaved scans are traversed left + // to right, top to bottom: + // 0 1 2 3 + // 4 5 6 7 + // Only DC scans (zigStart == 0) can be interleave AC scans must have + // only one component. + // To further complicate matters, for non-interleaved scans, there is no + // data for any blocks that are inside the image at the MCU level but + // outside the image at the pixel level. For example, a 24x16 pixel 4:2:0 + // progressive image consists of two 16x16 MCUs. The interleaved scans + // will process 8 Y blocks: + // 0 1 4 5 + // 2 3 6 7 + // The non-interleaved scans will process only 6 Y blocks: + // 0 1 2 + // 3 4 5 + if (lnComp != 1) + { + bx = (hi * mx) + (j % hi); + by = (vi * my) + (j / hi); + } + else + { + int q = mxx * hi; + bx = blockCount % q; + by = blockCount / q; + blockCount++; + if (bx * 8 >= this.imageWidth || by * 8 >= this.imageHeight) + { + continue; + } + } + + // Load the previous partially decoded coefficients, if applicable. + b = this.isProgressive ? this.progCoeffs[compIndex][((@by * mxx) * hi) + bx] : new Block(); + + if (ah != 0) + { + this.Refine(b, this.huffmanTrees[acTable, scan[i].AcTableSelector], zigStart, zigEnd, 1 << al); + } + else + { + int zig = zigStart; + if (zig == 0) + { + zig++; + + // Decode the DC coefficient, as specified in section F.2.2.1. + byte value = this.DecodeHuffman(this.huffmanTrees[dcTable, scan[i].DcTableSelector]); + if (value > 16) + { + throw new ImageFormatException("Excessive DC component"); + } + + int dcDelta = this.ReceiveExtend(value); + dc[compIndex] += dcDelta; + b[0] = dc[compIndex] << al; + } + + if (zig <= zigEnd && this.eobRun > 0) + { + this.eobRun--; + } + else + { + // Decode the AC coefficients, as specified in section F.2.2.2. + Huffman huffv = this.huffmanTrees[acTable, scan[i].AcTableSelector]; + for (; zig <= zigEnd; zig++) + { + byte value = this.DecodeHuffman(huffv); + byte val0 = (byte)(value >> 4); + byte val1 = (byte)(value & 0x0f); + if (val1 != 0) + { + zig += val0; + if (zig > zigEnd) + { + break; + } + + int ac = this.ReceiveExtend(val1); + b[Unzig[zig]] = ac << al; + } + else + { + if (val0 != 0x0f) + { + this.eobRun = (ushort)(1 << val0); + if (val0 != 0) + { + this.eobRun |= (ushort)this.DecodeBits(val0); + } + + this.eobRun--; + break; + } + + zig += 0x0f; + } + } + } + } + + if (this.isProgressive) + { + if (zigEnd != Block.BlockSize - 1 || al != 0) + { + // We haven't completely decoded this 8x8 block. Save the coefficients. + this.progCoeffs[compIndex][((by * mxx) * hi) + bx] = b; + + // At this point, we could execute the rest of the loop body to dequantize and + // perform the inverse DCT, to save early stages of a progressive image to the + // *image.YCbCr buffers (the whole point of progressive encoding), but in Go, + // the jpeg.Decode function does not return until the entire image is decoded, + // so we "continue" here to avoid wasted computation. + continue; + } + } + + // Dequantize, perform the inverse DCT and store the block to the image. + for (int zig = 0; zig < Block.BlockSize; zig++) + { + b[Unzig[zig]] *= qt[zig]; + } + + IDCT.Transform(b); + + byte[] dst; + int offset; + int stride; + + if (this.componentCount == 1) + { + dst = this.grayImage.Pixels; + stride = this.grayImage.Stride; + offset = this.grayImage.Offset + (8 * ((by * this.grayImage.Stride) + bx)); + } + else + { + switch (compIndex) + { + case 0: + dst = this.ycbcrImage.YChannel; + stride = this.ycbcrImage.YStride; + offset = this.ycbcrImage.YOffset + (8 * ((by * this.ycbcrImage.YStride) + bx)); + break; + + case 1: + dst = this.ycbcrImage.CbChannel; + stride = this.ycbcrImage.CStride; + offset = this.ycbcrImage.COffset + (8 * ((by * this.ycbcrImage.CStride) + bx)); + break; + + case 2: + dst = this.ycbcrImage.CrChannel; + stride = this.ycbcrImage.CStride; + offset = this.ycbcrImage.COffset + (8 * ((by * this.ycbcrImage.CStride) + bx)); + break; + + case 3: + + dst = this.blackPixels; + stride = this.blackStride; + offset = 8 * ((by * this.blackStride) + bx); + break; + + default: + throw new ImageFormatException("Too many components"); + } + } + + // Level shift by +128, clip to [0, 255], and write to dst. + for (int y = 0; y < 8; y++) + { + int y8 = y * 8; + int yStride = y * stride; + + for (int x = 0; x < 8; x++) + { + int c = b[y8 + x]; + if (c < -128) + { + c = 0; + } + else if (c > 127) + { + c = 255; + } + else + { + c += 128; + } + + dst[yStride + x + offset] = (byte)c; + } + } + } + + // for j + } + + // for i + mcu++; + + if (this.restartInterval > 0 && mcu % this.restartInterval == 0 && mcu < mxx * myy) + { + // A more sophisticated decoder could use RST[0-7] markers to resynchronize from corrupt input, + // but this one assumes well-formed input, and hence the restart marker follows immediately. + this.ReadFull(this.temp, 0, 2); + if (this.temp[0] != 0xff || this.temp[1] != expectedRst) + { + throw new ImageFormatException("Bad RST marker"); + } + + expectedRst++; + if (expectedRst == JpegConstants.Markers.RST7 + 1) + { + expectedRst = JpegConstants.Markers.RST0; + } + + // Reset the Huffman decoder. + this.bits = new Bits(); + + // Reset the DC components, as per section F.2.1.3.1. + dc = new int[MaxComponents]; + + // Reset the progressive decoder state, as per section G.1.2.2. + this.eobRun = 0; + } + } + + // for mx + } + + // for my + } + + /// + /// Decodes a successive approximation refinement block, as specified in section G.1.2. + /// + /// + /// + /// + /// + /// + private void Refine(Block b, Huffman h, int zigStart, int zigEnd, int delta) + { + // Refining a DC component is trivial. + if (zigStart == 0) + { + if (zigEnd != 0) + { + throw new ImageFormatException("Invalid state for zig DC component"); + } + + bool bit = this.DecodeBit(); + if (bit) + { + b[0] |= delta; + } + + return; + } + + // Refining AC components is more complicated; see sections G.1.2.2 and G.1.2.3. + int zig = zigStart; + if (this.eobRun == 0) + { + for (; zig <= zigEnd; zig++) + { + bool done = false; + int z = 0; + byte val = this.DecodeHuffman(h); + int val0 = val >> 4; + int val1 = val & 0x0f; + + switch (val1) + { + case 0: + if (val0 != 0x0f) + { + this.eobRun = (ushort)(1 << val0); + if (val0 != 0) + { + uint bits = this.DecodeBits(val0); + this.eobRun |= (ushort)bits; + } + + done = true; + } + + break; + case 1: + z = delta; + bool bit = this.DecodeBit(); + if (!bit) + { + z = -z; + } + + break; + default: + throw new ImageFormatException("Unexpected Huffman code"); + } + + if (done) + { + break; + } + + zig = this.RefineNonZeroes(b, zig, zigEnd, val0, delta); + if (zig > zigEnd) + { + throw new ImageFormatException($"Too many coefficients {zig} > {zigEnd}"); + } + + if (z != 0) + { + b[Unzig[zig]] = z; + } + } + } + + if (this.eobRun > 0) + { + this.eobRun--; + this.RefineNonZeroes(b, zig, zigEnd, -1, delta); + } + } + + // refineNonZeroes refines non-zero entries of b in zig-zag order. If nz >= 0, + // the first nz zero entries are skipped over. + private int RefineNonZeroes(Block b, int zig, int zigEnd, int nz, int delta) + { + for (; zig <= zigEnd; zig++) + { + int u = Unzig[zig]; + if (b[u] == 0) + { + if (nz == 0) + { + break; + } + + nz--; + continue; + } + + bool bit = this.DecodeBit(); + if (!bit) + { + continue; + } + + if (b[u] >= 0) + { + b[u] += delta; + } + else + { + b[u] -= delta; + } + } + + return zig; + } + + /// + /// Makes the image from the buffer. + /// + /// + /// + private void MakeImage(int mxx, int myy) + { + if (this.componentCount == 1) + { + GrayImage m = new GrayImage(8 * mxx, 8 * myy); + this.grayImage = m.Subimage(0, 0, this.imageWidth, this.imageHeight); + } + else + { + int h0 = this.componentArray[0].HorizontalFactor; + int v0 = this.componentArray[0].VerticalFactor; + int horizontalRatio = h0 / this.componentArray[1].HorizontalFactor; + int verticalRatio = v0 / this.componentArray[1].VerticalFactor; + + YCbCrImage.YCbCrSubsampleRatio ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444; + switch ((horizontalRatio << 4) | verticalRatio) + { + case 0x11: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444; + break; + case 0x12: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio440; + break; + case 0x21: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio422; + break; + case 0x22: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio420; + break; + case 0x41: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio411; + break; + case 0x42: + ratio = YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio410; + break; + } + + YCbCrImage m = new YCbCrImage(8 * h0 * mxx, 8 * v0 * myy, ratio); + this.ycbcrImage = m.Subimage(0, 0, this.imageWidth, this.imageHeight); + + if (this.componentCount == 4) + { + int h3 = this.componentArray[3].HorizontalFactor; + int v3 = this.componentArray[3].VerticalFactor; + this.blackPixels = new byte[8 * h3 * mxx * 8 * v3 * myy]; + this.blackStride = 8 * h3 * mxx; + } + } + } + + /// + /// Returns a value indicating whether the image in an RGB image. + /// + /// + /// The . + /// + private bool IsRGB() + { + if (this.isJfif) + { + return false; + } + + if (this.adobeTransformValid && this.adobeTransform == JpegConstants.Adobe.ColorTransformUnknown) + { + // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe + // says that 0 means Unknown (and in practice RGB) and 1 means YCbCr. + return true; + } + + return this.componentArray[0].Identifier == 'R' && this.componentArray[1].Identifier == 'G' && this.componentArray[2].Identifier == 'B'; + } + + /// + /// Represents a component scan + /// + private struct Scan + { + /// + /// Gets or sets the component index. + /// + public byte Index { get; set; } + + /// + /// Gets or sets the DC table selector + /// + public byte DcTableSelector { get; set; } + + /// + /// Gets or sets the AC table selector + /// + public byte AcTableSelector { get; set; } + } + + /// + /// The missing ff00 exception. + /// + private class MissingFF00Exception : Exception + { + } + + /// + /// The short huffman data exception. + /// + private class ShortHuffmanDataException : Exception + { + } + + private class EOFException : Exception + { + } + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id b/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id deleted file mode 100644 index e736cb248e..0000000000 --- a/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -cee4b682b4cfc0f0cef3cafb5b089c780d1cb4d1 \ No newline at end of file diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs index 69ea26a866..de3bf0dfc6 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -346,7 +346,7 @@ namespace ImageProcessorCore.Formats List result = new List(); - foreach (var encodedScanline in filteredScanlines) + foreach (byte[] encodedScanline in filteredScanlines) { result.AddRange(encodedScanline); } @@ -366,17 +366,25 @@ namespace ImageProcessorCore.Formats { List> candidates = new List>(); - byte[] sub = SubFilter.Encode(rawScanline, byteCount); - candidates.Add(new Tuple(sub, this.CalculateTotalVariation(sub))); + if (this.PngColorType == PngColorType.Palette) + { + byte[] none = NoneFilter.Encode(rawScanline); + candidates.Add(new Tuple(none, this.CalculateTotalVariation(none))); + } + else + { + byte[] sub = SubFilter.Encode(rawScanline, byteCount); + candidates.Add(new Tuple(sub, this.CalculateTotalVariation(sub))); - byte[] up = UpFilter.Encode(rawScanline, previousScanline); - candidates.Add(new Tuple(up, this.CalculateTotalVariation(up))); + byte[] up = UpFilter.Encode(rawScanline, previousScanline); + candidates.Add(new Tuple(up, this.CalculateTotalVariation(up))); - byte[] average = AverageFilter.Encode(rawScanline, previousScanline, byteCount); - candidates.Add(new Tuple(average, this.CalculateTotalVariation(average))); + byte[] average = AverageFilter.Encode(rawScanline, previousScanline, byteCount); + candidates.Add(new Tuple(average, this.CalculateTotalVariation(average))); - byte[] paeth = PaethFilter.Encode(rawScanline, previousScanline, byteCount); - candidates.Add(new Tuple(paeth, this.CalculateTotalVariation(paeth))); + byte[] paeth = PaethFilter.Encode(rawScanline, previousScanline, byteCount); + candidates.Add(new Tuple(paeth, this.CalculateTotalVariation(paeth))); + } int lowestTotalVariation = int.MaxValue; int lowestTotalVariationIndex = 0; @@ -603,7 +611,7 @@ namespace ImageProcessorCore.Formats /// The stream. private void WriteDataChunks(Stream stream) { - byte[] data = this.EncodePixelData(); + byte[] data = this.EncodePixelData(); byte[] buffer; int bufferLength; diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/Car.bmp b/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/Car.bmp new file mode 100644 index 0000000000..edaf3a8e48 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/Car.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d3a4a30cd67db6ded1e57126c7ba275404703e64b3dfb1c9c711128c15b0124 +size 810054 diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/Car.bmp.REMOVED.git-id b/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/Car.bmp.REMOVED.git-id deleted file mode 100644 index e6132fd8c2..0000000000 --- a/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/Car.bmp.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -edd8ac1feb2c4649e6f5aa80af8d4cf33642a546 \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/neg_height.bmp b/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/neg_height.bmp new file mode 100644 index 0000000000..d0b99a9025 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/neg_height.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81437b88a5d92fcb545fae4991643a0c73d95d0277dac0b79074971780008c8c +size 6220854 diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/neg_height.bmp.REMOVED.git-id b/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/neg_height.bmp.REMOVED.git-id deleted file mode 100644 index 5440f235d0..0000000000 --- a/tests/ImageProcessorCore.Tests/TestImages/Formats/Bmp/neg_height.bmp.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -8f864b82464c595d7a81087d018db9994506e221 \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Calliphora.jpg b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Calliphora.jpg new file mode 100644 index 0000000000..aa3fdef017 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Calliphora.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67172fcab405f914587b88cd1106328e6b24ab59d622ba509dcc99509951ff5c +size 254766 diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Calliphora.jpg.REMOVED.git-id b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Calliphora.jpg.REMOVED.git-id deleted file mode 100644 index 44bed7ad38..0000000000 --- a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Calliphora.jpg.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -5d446f64d0636f6ad7e9f82625eeff89ef394fe2 \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Floorplan.jpeg b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Floorplan.jpeg new file mode 100644 index 0000000000..6f439d2207 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Floorplan.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de00b34b78dfa0886c93d8dd5cede27b4940d5c620e44631e77e6dc8838befc3 +size 161577 diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Floorplan.jpeg.REMOVED.git-id b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Floorplan.jpeg.REMOVED.git-id deleted file mode 100644 index 88d957b218..0000000000 --- a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/Floorplan.jpeg.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -5a1eaf806b7f3793d3db41c85bac6366584bce83 \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/cmyk.jpg b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/cmyk.jpg new file mode 100644 index 0000000000..2fe8f0a61d --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/cmyk.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33e3546a64df7fa1d528441926421b193e399a83490a6307762fb7eee9640bf0 +size 611572 diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/cmyk.jpg.REMOVED.git-id b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/cmyk.jpg.REMOVED.git-id deleted file mode 100644 index fb900876ac..0000000000 --- a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/cmyk.jpg.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -30e88773ededd890ad97f0a5cfac372e1af07aa5 \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg new file mode 100644 index 0000000000..c305caef46 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d92a88b04518e266b98d9d2f5b4eb88f3f91c332d3397ea859bab8cabc41185 +size 84887 diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg.REMOVED.git-id b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg.REMOVED.git-id deleted file mode 100644 index c6c87c96c7..0000000000 --- a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -56cbc3371def2882d1ead5d4d2456550f2b8d72c \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/blur.png b/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/blur.png new file mode 100644 index 0000000000..2ac488b7cc --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/blur.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10df946d3d6a9832bacd9ac2587b890c348d17731412c8fd17c34f66f35d9c94 +size 183768 diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id b/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id deleted file mode 100644 index 7a8a58268f..0000000000 --- a/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -f3c65955eae84d00c5a0343c7e2aa3610a7d9df7 \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/splash.png b/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/splash.png new file mode 100644 index 0000000000..ca4f86bced --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/splash.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4c13422913f1c1910f8dd607236e79b4a5c7053deb8ce1c8be8372eca7465fb +size 245033 diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/splash.png.REMOVED.git-id b/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/splash.png.REMOVED.git-id deleted file mode 100644 index 5388959b91..0000000000 --- a/tests/ImageProcessorCore.Tests/TestImages/Formats/Png/splash.png.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -0964ae9744027325bd6a2a19b2247c39f8349ca1 \ No newline at end of file