From 1555f09baa63350bc53ad0629547ae36e4b34eb1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 16 Jun 2017 22:56:58 +1000 Subject: [PATCH 01/43] Begin port --- .../Common/Extensions/StreamExtensions.cs | 2 +- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 10 +- .../Formats/Jpeg/Port/Components/Component.cs | 53 ++ .../Formats/Jpeg/Port/Components/Frame.cs | 73 +++ .../Jpeg/Port/Components/HuffmanTables.cs | 15 + .../Port/Components/QuantizationTables.cs | 43 ++ src/ImageSharp/Formats/Jpeg/Port/Huffman.cs | 6 + .../Formats/Jpeg/Port/JpegConstants.cs | 185 +++++++ .../Formats/Jpeg/Port/JpegDecoderCore.cs | 522 ++++++++++++++++++ src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs | 33 ++ src/ImageSharp/Memory/Fast2DArray{T}.cs | 32 +- 11 files changed, 967 insertions(+), 7 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Huffman.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs index 6de94dd22..6b474b92e 100644 --- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs +++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs @@ -27,7 +27,7 @@ namespace ImageSharp if (stream.CanSeek) { - stream.Position += count; + stream.Seek(count, SeekOrigin.Current); // Position += count; } else { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 56d025504..e900e51ac 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -21,10 +21,12 @@ namespace ImageSharp.Formats { Guard.NotNull(stream, "stream"); - using (JpegDecoderCore decoder = new JpegDecoderCore(options, configuration)) - { - return decoder.Decode(stream); - } + // using (JpegDecoderCore decoder = new JpegDecoderCore(options, configuration)) + // { + // return decoder.Decode(stream); + // } + var decoder = new Jpeg.Port.JpegDecoderCore(options, configuration); + return decoder.Decode(stream); } } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs new file mode 100644 index 000000000..524e3b913 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + /// + /// Represents a single color component + /// + internal struct Component + { + /// + /// Gets or sets the component Id + /// + public byte Id; + + /// + /// Gets or sets the horizontal sampling factor. + /// + public int HorizontalFactor; + + /// + /// Gets or sets the vertical sampling factor. + /// + public int VerticalFactor; + + /// + /// Gets or sets the identifier + /// + public byte QuantizationIdentifier; + + /// + /// Gets or sets the quantization table + /// + public short[] QuantizationTable; + + /// + /// Gets or sets the block data + /// + public short[] BlockData; + + /// + /// Gets or sets the number of blocks per line + /// + public int BlocksPerLine; + + /// + /// Gets or sets the number of blocks per column + /// + public int BlocksPerColumn; + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs new file mode 100644 index 000000000..7e72df8b0 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + /// + /// Represent a single jpeg frame + /// + internal class Frame + { + /// + /// Gets or sets a value indicating whether the frame uses the extended specification + /// + public bool Extended { get; set; } + + /// + /// Gets or sets a value indicating whether the frame uses the progressive specification + /// + public bool Progressive { get; set; } + + /// + /// Gets or sets the precision + /// + public byte Precision { get; set; } + + /// + /// Gets or sets the number of scanlines within the frame + /// + public short Scanlines { get; set; } + + /// + /// Gets or sets the number of samples per scanline + /// + public short SamplesPerLine { get; set; } + + /// + /// Gets or sets the number of components within a frame. In progressive frames this value can range from only 1 to 4 + /// + public byte ComponentCount { get; set; } + + /// + /// Gets or sets the component id collection + /// + public byte[] ComponentIds { get; set; } + + /// + /// Gets or sets the frame component collection + /// + public Component[] Components { get; set; } + + /// + /// Gets or sets the maximum horizontal sampling factor + /// + public int MaxHorizontalFactor { get; set; } + + /// + /// Gets or sets the maximum vertical sampling factor + /// + public int MaxVerticalFactor { get; set; } + + /// + /// Gets or sets the number of MCU's per line + /// + public int McusPerLine { get; set; } + + /// + /// Gets or sets the number of MCU's per column + /// + public int McusPerColumn { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs new file mode 100644 index 000000000..5c4ffce24 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -0,0 +1,15 @@ +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using ImageSharp.Memory; + + /// + /// Defines a pair of huffman tables + /// + internal class HuffmanTables + { + /// + /// Gets or sets the quantization tables. + /// + public Fast2DArray Tables { get; set; } = new Fast2DArray(256, 2); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs new file mode 100644 index 000000000..f808eecfd --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using ImageSharp.Memory; + + /// + /// Contains the quantization tables. + /// TODO: This all needs optimizing for memory. I'm just stubbing out functionality for now. + /// + internal class QuantizationTables + { + /// + /// Gets the ZigZag scan table + /// + public static byte[] DctZigZag { get; } = + { + 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 + }; + + /// + /// Gets or sets the quantization tables. + /// + public Fast2DArray Tables { get; set; } = new Fast2DArray(64, 4); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Huffman.cs b/src/ImageSharp/Formats/Jpeg/Port/Huffman.cs new file mode 100644 index 000000000..75b6dc562 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Huffman.cs @@ -0,0 +1,6 @@ +namespace ImageSharp.Formats.Jpeg.Port +{ + class Huffman + { + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs new file mode 100644 index 000000000..236e38f96 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs @@ -0,0 +1,185 @@ +// ReSharper disable InconsistentNaming +namespace ImageSharp.Formats.Jpeg.Port +{ + /// + /// Contains jpeg constant values + /// + internal static class JpegConstants + { + /// + /// Contains marker specific constants + /// + public static class Markers + { + /// + /// The prefix used for all markers. + /// + public const byte Prefix = 0xFF; + + /// + /// The Start of Image marker + /// + public const ushort SOI = 0xFFD8; + + /// + /// The End of Image marker + /// + public const ushort EOI = 0xFFD9; + + /// + /// Application specific marker for marking the jpeg format. + /// + /// + public const ushort APP0 = 0xFFE0; + + /// + /// Application specific marker for marking where to store metadata. + /// + public const ushort APP1 = 0xFFE1; + + /// + /// Application specific marker for marking where to store ICC profile information. + /// + public const ushort APP2 = 0xFFE2; + + /// + /// Application specific marker. + /// + public const ushort APP3 = 0xFFE3; + + /// + /// Application specific marker. + /// + public const ushort APP4 = 0xFFE4; + + /// + /// Application specific marker. + /// + public const ushort APP5 = 0xFFE5; + + /// + /// Application specific marker. + /// + public const ushort APP6 = 0xFFE6; + + /// + /// Application specific marker. + /// + public const ushort APP7 = 0xFFE7; + + /// + /// Application specific marker. + /// + public const ushort APP8 = 0xFFE8; + + /// + /// Application specific marker. + /// + public const ushort APP9 = 0xFFE9; + + /// + /// Application specific marker. + /// + public const ushort APP10 = 0xFFEA; + + /// + /// Application specific marker. + /// + public const ushort APP11 = 0xFFEB; + + /// + /// Application specific marker. + /// + public const ushort APP12 = 0xFFEC; + + /// + /// Application specific marker. + /// + public const ushort APP13 = 0xFFED; + + /// + /// Application specific marker used by Adobe for storing encoding information for DCT filters. + /// + public const ushort APP14 = 0xFFEE; + + /// + /// Application specific marker used by GraphicConverter to store JPEG quality. + /// + public const ushort APP15 = 0xFFEF; + + /// + /// The text comment marker + /// + public const ushort COM = 0xFFFE; + + /// + /// Define Quantization Table(s) marker + /// + /// Specifies one or more quantization tables. + /// + /// + public const ushort DQT = 0xFFDB; + + /// + /// Start of Frame (baseline DCT) + /// + /// Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const ushort SOF0 = 0xFFC0; + + /// + /// Start Of Frame (Extended Sequential DCT) + /// + /// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const ushort SOF1 = 0xFFC1; + + /// + /// Start Of Frame (progressive DCT) + /// + /// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, + /// and component subsampling (e.g., 4:2:0). + /// + /// + public const ushort SOF2 = 0xFFC2; + + /// + /// Define Huffman Table(s) + /// + /// Specifies one or more Huffman tables. + /// + /// + public const ushort DHT = 0xFFC4; + + /// + /// Contains JFIF specific markers + /// + public static class JFif + { + /// + /// Represents J in ASCII + /// + public const byte J = 0x4A; + + /// + /// Represents F in ASCII + /// + public const byte F = 0x46; + + /// + /// Represents I in ASCII + /// + public const byte I = 0x49; + + /// + /// Represents the null "0" marker + /// + public const byte Null = 0; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs new file mode 100644 index 000000000..bb2d09938 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -0,0 +1,522 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port +{ + using System; + using System.Buffers; + using System.IO; + + using ImageSharp.Formats.Jpeg.Port.Components; + using ImageSharp.PixelFormats; + + /// + /// Performs the jpeg decoding operation. + /// + internal class JpegDecoderCore + { + /// + /// The decoder options. + /// + private readonly IDecoderOptions options; + + /// + /// The global configuration + /// + private readonly Configuration configuration; + + /// + /// Gets the temporary buffer used to store bytes read from the stream. + /// + private readonly byte[] temp = new byte[2 * 16 * 4]; + + private readonly byte[] uint16Buffer = new byte[2]; + + private QuantizationTables quantizationTables; + + private HuffmanTables dcHuffmanTables; + + private HuffmanTables acHuffmanTables; + + private Frame frame; + + /// + /// COntains information about the jFIF marker + /// + private JFif jFif; + + /// + /// Whether the image has a EXIF header + /// + private bool isExif; + + private int offset; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + /// The configuration. + public JpegDecoderCore(IDecoderOptions options, Configuration configuration) + { + this.configuration = configuration ?? Configuration.Default; + this.options = options ?? new DecoderOptions(); + } + + /// + /// Gets the input stream. + /// + public Stream InputStream { get; private set; } + + /// + /// Decodes the image from the specified and sets + /// the data to image. + /// + /// The pixel format. + /// The stream, where the image should be. + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : struct, IPixel + { + this.InputStream = stream; + this.ParseStream(); + + var image = new Image(1, 1); + return image; + } + + private void ParseStream() + { + // Check for the Start Of Image marker. + ushort fileMarker = this.ReadUint16(); + if (fileMarker != JpegConstants.Markers.SOI) + { + throw new ImageFormatException("Missing SOI marker."); + } + + fileMarker = this.ReadUint16(); + + this.quantizationTables = new QuantizationTables(); + + while (fileMarker != JpegConstants.Markers.EOI) + { + // Get the marker length + int remaining = this.ReadUint16() - 2; + + switch (fileMarker) + { + case JpegConstants.Markers.APP0: + this.ProcessApplicationHeaderMarker(remaining); + break; + + case JpegConstants.Markers.APP1: + case JpegConstants.Markers.APP2: + case JpegConstants.Markers.APP3: + case JpegConstants.Markers.APP4: + case JpegConstants.Markers.APP5: + case JpegConstants.Markers.APP6: + case JpegConstants.Markers.APP7: + case JpegConstants.Markers.APP8: + case JpegConstants.Markers.APP9: + case JpegConstants.Markers.APP10: + case JpegConstants.Markers.APP11: + case JpegConstants.Markers.APP12: + case JpegConstants.Markers.APP13: + case JpegConstants.Markers.APP14: + case JpegConstants.Markers.APP15: + case JpegConstants.Markers.COM: + break; + + case JpegConstants.Markers.DQT: + this.ProcessDqtMarker(remaining); + break; + + case JpegConstants.Markers.SOF0: + case JpegConstants.Markers.SOF1: + case JpegConstants.Markers.SOF2: + this.ProcessStartOfFrameMarker(remaining, fileMarker); + break; + + case JpegConstants.Markers.DHT: + this.ProcessDefineHuffmanTablesMarker(remaining); + break; + } + + // Read on + fileMarker = this.FindNextFileMarker(); + } + } + + /// + /// Processes the application header containing the JFIF identifier plus extra data. + /// + /// The remaining bytes in the segment block. + private void ProcessApplicationHeaderMarker(int remaining) + { + if (remaining < 5) + { + // Skip the application header length + this.InputStream.Skip(remaining); + return; + } + + this.InputStream.Read(this.temp, 0, 13); + remaining -= 13; + + bool isJfif = this.temp[0] == JpegConstants.Markers.JFif.J && + this.temp[1] == JpegConstants.Markers.JFif.F && + this.temp[2] == JpegConstants.Markers.JFif.I && + this.temp[3] == JpegConstants.Markers.JFif.F && + this.temp[4] == JpegConstants.Markers.JFif.Null; + + if (isJfif) + { + this.jFif = new JFif + { + MajorVersion = this.temp[5], + MinorVersion = this.temp[6], + DensityUnits = this.temp[7], + XDensity = (short)((this.temp[8] << 8) | this.temp[9]), + YDensity = (short)((this.temp[10] << 8) | this.temp[11]) + }; + } + + // Skip thumbnails for now. + if (remaining > 0) + { + this.InputStream.Skip(remaining); + } + } + + /// + /// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1. + /// + /// The remaining bytes in the segment block. + /// + /// Thrown if the tables do not match the header + /// + private void ProcessDqtMarker(int remaining) + { + while (remaining > 0) + { + bool done = false; + remaining--; + int quantizationTableSpec = this.InputStream.ReadByte(); + + if (quantizationTableSpec > 3) + { + throw new ImageFormatException("Bad Tq index value"); + } + + switch (quantizationTableSpec >> 4) + { + case 0: + { + // 8 bit values + if (remaining < 64) + { + done = true; + break; + } + + this.InputStream.Read(this.temp, 0, 64); + remaining -= 64; + + Span tableSpan = this.quantizationTables.Tables.GetRowSpan(quantizationTableSpec); + for (int j = 0; j < 64; j++) + { + tableSpan[QuantizationTables.DctZigZag[j]] = this.temp[j]; + } + } + + break; + case 1: + { + // 16 bit values + if (remaining < 128) + { + done = true; + break; + } + + this.InputStream.Read(this.temp, 0, 128); + remaining -= 128; + + Span tableSpan = this.quantizationTables.Tables.GetRowSpan(quantizationTableSpec); + for (int j = 0; j < 64; j++) + { + tableSpan[QuantizationTables.DctZigZag[j]] = (short)((this.temp[2 * j] << 8) | this.temp[(2 * j) + 1]); + } + } + + break; + default: + throw new ImageFormatException("Bad Tq index value"); + } + + if (done) + { + break; + } + } + + if (remaining != 0) + { + throw new ImageFormatException("DQT has wrong length"); + } + } + + /// + /// Processes the Start of Frame marker. Specified in section B.2.2. + /// + /// The remaining bytes in the segment block. + /// The current frame marker. + private void ProcessStartOfFrameMarker(int remaining, ushort frameMarker) + { + if (this.frame != null) + { + throw new ImageFormatException("Multiple SOF markers. Only single frame jpegs supported."); + } + + this.InputStream.Read(this.temp, 0, remaining); + + this.frame = new Frame + { + Extended = frameMarker == JpegConstants.Markers.SOF1, + Progressive = frameMarker == JpegConstants.Markers.SOF1, + Precision = this.temp[0], + Scanlines = (short)((this.temp[1] << 8) | this.temp[2]), + SamplesPerLine = (short)((this.temp[3] << 8) | this.temp[4]), + ComponentCount = this.temp[5] + }; + + int maxH = 0; + int maxV = 0; + int index = 6; + + // TODO: Pool this. + this.frame.ComponentIds = new byte[this.frame.ComponentCount]; + this.frame.Components = new Component[this.frame.ComponentCount]; + + for (int i = 0; i < this.frame.ComponentCount; i++) + { + int h = this.temp[index + 1] >> 4; + int v = this.temp[index + 1] & 15; + + if (maxH < h) + { + maxH = h; + } + + if (maxV < v) + { + maxV = v; + } + + ref var component = ref this.frame.Components[i]; + component.Id = this.temp[index]; + component.HorizontalFactor = h; + component.VerticalFactor = v; + component.QuantizationIdentifier = this.temp[index + 2]; + + // Don't assign the table yet. + index += 3; + } + + this.frame.MaxHorizontalFactor = maxH; + this.frame.MaxVerticalFactor = maxV; + this.PrepareComponents(); + } + + /// + /// Processes a Define Huffman Table marker, and initializes a huffman + /// struct from its contents. Specified in section B.2.4.2. + /// + /// The remaining bytes in the segment block. + private void ProcessDefineHuffmanTablesMarker(int remaining) + { + if (remaining < 17) + { + throw new ImageFormatException("DHT has wrong length"); + } + + // TODO: Pool this + byte[] huffmanData = new byte[remaining]; + this.InputStream.Read(huffmanData, 0, remaining); + + int o = 0; + for (int i = 0; i < remaining;) + { + byte huffmanTableSpec = huffmanData[i]; + byte[] codeLengths = new byte[16]; + int codeLengthSum = 0; + + for (int j = 0; j < 16; j++) + { + codeLengthSum += codeLengths[j] = huffmanData[o++]; + } + + short[] huffmanValues = new short[codeLengthSum]; + + byte[] values = null; + try + { + values = ArrayPool.Shared.Rent(256); + this.InputStream.Read(values, 0, codeLengthSum); + + for (int j = 0; j < codeLengthSum; j++) + { + huffmanValues[j] = values[o++]; + } + } + finally + { + if (values != null) + { + ArrayPool.Shared.Return(values); + } + } + + i += 17 + codeLengthSum; + + this.BuildHuffmanTable( + huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, + huffmanTableSpec & 15, + codeLengths, + huffmanValues); + } + } + + private void BuildHuffmanTable(HuffmanTables tables, int index, byte[] codeLengths, short[] values) + { + Span tableSpan = tables.Tables.GetRowSpan(index); + } + + /// + /// Allocates the frame component blocks + /// + private void PrepareComponents() + { + int mcusPerLine = this.frame.SamplesPerLine / 8 / this.frame.MaxHorizontalFactor; + int mcusPerColumn = this.frame.Scanlines / 8 / this.frame.MaxVerticalFactor; + + for (int i = 0; i < this.frame.ComponentCount; i++) + { + ref var component = ref this.frame.Components[i]; + int blocksPerLine = this.frame.SamplesPerLine / 8 * component.HorizontalFactor / this.frame.MaxHorizontalFactor; + int blocksPerColumn = this.frame.Scanlines / 8 * component.VerticalFactor / this.frame.MaxVerticalFactor; + int blocksPerLineForMcu = mcusPerLine * component.HorizontalFactor; + int blocksPerColumnForMcu = mcusPerColumn * component.VerticalFactor; + + int blocksBufferSize = 64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1); + + // TODO: Pool this + component.BlockData = new short[blocksBufferSize]; + component.BlocksPerLine = blocksPerLine; + component.BlocksPerColumn = blocksPerColumn; + } + + this.frame.McusPerLine = mcusPerLine; + this.frame.McusPerColumn = mcusPerColumn; + } + + /// + /// Finds the next file marker within the byte stream + /// + /// The + private ushort FindNextFileMarker() + { + while (true) + { + int value = this.InputStream.Read(this.uint16Buffer, 0, 2); + + if (value <= 0) + { + return JpegConstants.Markers.EOI; + } + + while (this.uint16Buffer[0] != JpegConstants.Markers.Prefix) + { + // 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.uint16Buffer[0] = this.uint16Buffer[1]; + + value = this.InputStream.ReadByte(); + if (value <= 0) + { + return JpegConstants.Markers.EOI; + } + + this.uint16Buffer[1] = (byte)value; + } + + return (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); + } + } + + /// + /// Reads a from the stream advancing it by two bytes + /// + /// The + private ushort ReadUint16() + { + this.InputStream.Read(this.uint16Buffer, 0, 2); + ushort value = (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); + this.offset += 2; + return value; + } + + /// + /// Provides information about the JFIF marker segment + /// + internal struct JFif + { + /// + /// The major version + /// + public byte MajorVersion; + + /// + /// The minor version + /// + public byte MinorVersion; + + /// + /// Units for the following pixel density fields + /// 00 : No units; width:height pixel aspect ratio = Ydensity:Xdensity + /// 01 : Pixels per inch (2.54 cm) + /// 02 : Pixels per centimeter + /// + public byte DensityUnits; + + /// + /// Horizontal pixel density. Must not be zero. + /// + public short XDensity; + + /// + /// Vertical pixel density. Must not be zero. + /// + public short YDensity; + + // TODO: Thumbnail? + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs new file mode 100644 index 000000000..a279339e7 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs @@ -0,0 +1,33 @@ +namespace ImageSharp.Formats.Jpeg.Port +{ + /// + /// Represents a jpeg frame + /// + internal class JpegFrame + { + /// + /// Gets or sets a value indicating whether the fame is extended + /// + public bool Extended { get; set; } + + /// + /// Gets or sets a value indicating whether the fame is progressive + /// + public bool Progressive { get; set; } + + /// + /// Gets or sets the precision + /// + public byte Precision { get; set; } + + /// + /// Gets or sets the number of scanlines within the frame + /// + public short Scanlines { get; set; } + + /// + /// Gets or sets the number of samples per scanline + /// + public short SamplesPerLine { get; set; } + } +} diff --git a/src/ImageSharp/Memory/Fast2DArray{T}.cs b/src/ImageSharp/Memory/Fast2DArray{T}.cs index 260c829e2..ee8a60983 100644 --- a/src/ImageSharp/Memory/Fast2DArray{T}.cs +++ b/src/ImageSharp/Memory/Fast2DArray{T}.cs @@ -106,11 +106,23 @@ namespace ImageSharp.Memory return new Fast2DArray(data); } + /// + /// Gets a representing the row beginning from the the first item on that row. + /// + /// The y-coordinate of the row. Must be greater than or equal to zero and less than the height of the 2D array. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetRowSpan(int row) + { + this.CheckCoordinates(row); + return new Span(this.Data, row * this.Width, this.Width); + } + /// /// Checks the coordinates to ensure they are within bounds. /// - /// The row-coordinate of the item. Must be greater than zero and smaller than the height of the array. - /// The column-coordinate of the item. Must be greater than zero and smaller than the width of the array. + /// The y-coordinate of the item. Must be greater than zero and smaller than the height of the array. + /// The x-coordinate of the item. Must be greater than zero and smaller than the width of the array. /// /// Thrown if the coordinates are not within the bounds of the array. /// @@ -127,5 +139,21 @@ namespace ImageSharp.Memory throw new ArgumentOutOfRangeException(nameof(column), column, $"{column} is outwith the array bounds."); } } + + /// + /// Checks the coordinates to ensure they are within bounds. + /// + /// The y-coordinate of the item. Must be greater than zero and smaller than the height of the array. + /// + /// Thrown if the coordinates are not within the bounds of the image. + /// + [Conditional("DEBUG")] + private void CheckCoordinates(int row) + { + if (row < 0 || row >= this.Height) + { + throw new ArgumentOutOfRangeException(nameof(row), row, $"{row} is outwith the array bounds."); + } + } } } \ No newline at end of file From 1abe63113d81b1dfae2c779cc733630661c319cf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 19 Jun 2017 02:01:12 +1000 Subject: [PATCH 02/43] Fix header finder --- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index bb2d09938..50ac407a4 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -10,6 +10,7 @@ namespace ImageSharp.Formats.Jpeg.Port using System.IO; using ImageSharp.Formats.Jpeg.Port.Components; + using ImageSharp.Memory; using ImageSharp.PixelFormats; /// @@ -99,6 +100,8 @@ namespace ImageSharp.Formats.Jpeg.Port fileMarker = this.ReadUint16(); this.quantizationTables = new QuantizationTables(); + this.dcHuffmanTables = new HuffmanTables(); + this.acHuffmanTables = new HuffmanTables(); while (fileMarker != JpegConstants.Markers.EOI) { @@ -342,55 +345,62 @@ namespace ImageSharp.Formats.Jpeg.Port throw new ImageFormatException("DHT has wrong length"); } - // TODO: Pool this - byte[] huffmanData = new byte[remaining]; - this.InputStream.Read(huffmanData, 0, remaining); - - int o = 0; - for (int i = 0; i < remaining;) + using (var huffmanData = new Buffer(remaining)) { - byte huffmanTableSpec = huffmanData[i]; - byte[] codeLengths = new byte[16]; - int codeLengthSum = 0; + this.InputStream.Read(huffmanData.Array, 1, remaining); - for (int j = 0; j < 16; j++) + int o = 0; + for (int i = 0; i < remaining;) { - codeLengthSum += codeLengths[j] = huffmanData[o++]; - } + byte huffmanTableSpec = huffmanData[i]; + byte[] codeLengths = new byte[16]; + int codeLengthSum = 0; - short[] huffmanValues = new short[codeLengthSum]; + for (int j = 0; j < 16; j++) + { + codeLengthSum += codeLengths[j] = huffmanData[o++]; + } - byte[] values = null; - try - { - values = ArrayPool.Shared.Rent(256); - this.InputStream.Read(values, 0, codeLengthSum); + short[] huffmanValues = new short[codeLengthSum]; - for (int j = 0; j < codeLengthSum; j++) + byte[] values = null; + try { - huffmanValues[j] = values[o++]; + values = ArrayPool.Shared.Rent(256); + this.InputStream.Read(values, 0, codeLengthSum); + + for (int j = 0; j < codeLengthSum; j++) + { + huffmanValues[j] = values[o++]; + } } - } - finally - { - if (values != null) + finally { - ArrayPool.Shared.Return(values); + if (values != null) + { + ArrayPool.Shared.Return(values); + } } - } - i += 17 + codeLengthSum; + i += 17 + codeLengthSum; - this.BuildHuffmanTable( - huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, - huffmanTableSpec & 15, - codeLengths, - huffmanValues); + this.BuildHuffmanTable( + huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, + huffmanTableSpec & 15, + codeLengths, + huffmanValues); + } } } private void BuildHuffmanTable(HuffmanTables tables, int index, byte[] codeLengths, short[] values) { + int length = 16; + while (length > 0 && codeLengths[length - 1] == 0) + { + length--; + } + Span tableSpan = tables.Tables.GetRowSpan(index); } @@ -432,7 +442,7 @@ namespace ImageSharp.Formats.Jpeg.Port { int value = this.InputStream.Read(this.uint16Buffer, 0, 2); - if (value <= 0) + if (value == 0) { return JpegConstants.Markers.EOI; } @@ -459,7 +469,7 @@ namespace ImageSharp.Formats.Jpeg.Port this.uint16Buffer[0] = this.uint16Buffer[1]; value = this.InputStream.ReadByte(); - if (value <= 0) + if (value == -1) { return JpegConstants.Markers.EOI; } From d4d74b43c56fbbee8a770795cc976dfa1dcb7659 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 19 Jun 2017 02:04:57 +1000 Subject: [PATCH 03/43] Add js source link --- src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 50ac407a4..3d36d61d9 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -15,6 +15,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Performs the jpeg decoding operation. + /// Ported from /// internal class JpegDecoderCore { From f63f85aaef42fbe31f14239ee2cbc12482d4d958 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 19 Jun 2017 02:08:24 +1000 Subject: [PATCH 04/43] Use buffer --- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 3d36d61d9..300cb7aef 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -362,26 +362,17 @@ namespace ImageSharp.Formats.Jpeg.Port codeLengthSum += codeLengths[j] = huffmanData[o++]; } + // TODO: Pooling? short[] huffmanValues = new short[codeLengthSum]; - - byte[] values = null; - try + using (var values = new Buffer(256)) { - values = ArrayPool.Shared.Rent(256); - this.InputStream.Read(values, 0, codeLengthSum); + this.InputStream.Read(values.Array, 0, codeLengthSum); for (int j = 0; j < codeLengthSum; j++) { huffmanValues[j] = values[o++]; } } - finally - { - if (values != null) - { - ArrayPool.Shared.Return(values); - } - } i += 17 + codeLengthSum; From 2c629c7fd82d47da6e21f01bdfd1556f362f3f55 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 19 Jun 2017 02:09:30 +1000 Subject: [PATCH 05/43] Remove offset --- src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 300cb7aef..b29446fdb 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -54,8 +54,6 @@ namespace ImageSharp.Formats.Jpeg.Port /// private bool isExif; - private int offset; - /// /// Initializes a new instance of the class. /// @@ -481,7 +479,6 @@ namespace ImageSharp.Formats.Jpeg.Port { this.InputStream.Read(this.uint16Buffer, 0, 2); ushort value = (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); - this.offset += 2; return value; } From b025ed6bb66a19c9bfdefe45ac72dab6efd4b102 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 19 Jun 2017 02:19:15 +1000 Subject: [PATCH 06/43] Fix progressive bool assignment --- src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index b29446fdb..1b2b8b99f 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -287,7 +287,7 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame = new Frame { Extended = frameMarker == JpegConstants.Markers.SOF1, - Progressive = frameMarker == JpegConstants.Markers.SOF1, + Progressive = frameMarker == JpegConstants.Markers.SOF2, Precision = this.temp[0], Scanlines = (short)((this.temp[1] << 8) | this.temp[2]), SamplesPerLine = (short)((this.temp[3] << 8) | this.temp[4]), From 3728b821ddf25f3738bc240b875d09398c7913c2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 20 Jun 2017 01:19:47 +1000 Subject: [PATCH 07/43] =?UTF-8?q?(=E2=95=AF=C2=B0=E2=96=A1=C2=B0=EF=BC=89?= =?UTF-8?q?=E2=95=AF=EF=B8=B5=20=E2=94=BB=E2=94=81=E2=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Jpeg/Port/Components/HuffmanTables.cs | 30 ++++++- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 83 +++++++++++++++---- 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs index 5c4ffce24..5fcc2007b 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -1,5 +1,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { + using System.Collections.Generic; + using ImageSharp.Memory; /// @@ -10,6 +12,32 @@ /// /// Gets or sets the quantization tables. /// - public Fast2DArray Tables { get; set; } = new Fast2DArray(256, 2); + public Fast2DArray Tables { get; set; } = new Fast2DArray(256, 2); + } + + internal struct HuffmanBranch + { + public HuffmanBranch(short value) + : this(value, new List()) + { + } + + public HuffmanBranch(List children) + : this(0, children) + { + } + + private HuffmanBranch(short value, List children) + { + this.Index = 0; + this.Value = value; + this.Children = children; + } + + public int Index; + + public short Value; + + public List Children; } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 1b2b8b99f..db25ba845 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -6,8 +6,9 @@ namespace ImageSharp.Formats.Jpeg.Port { using System; - using System.Buffers; + using System.Collections.Generic; using System.IO; + using System.Linq; using ImageSharp.Formats.Jpeg.Port.Components; using ImageSharp.Memory; @@ -346,9 +347,9 @@ namespace ImageSharp.Formats.Jpeg.Port using (var huffmanData = new Buffer(remaining)) { - this.InputStream.Read(huffmanData.Array, 1, remaining); + this.InputStream.Skip(1); + this.InputStream.Read(huffmanData.Array, 0, remaining); - int o = 0; for (int i = 0; i < remaining;) { byte huffmanTableSpec = huffmanData[i]; @@ -357,41 +358,95 @@ namespace ImageSharp.Formats.Jpeg.Port for (int j = 0; j < 16; j++) { - codeLengthSum += codeLengths[j] = huffmanData[o++]; + codeLengthSum += codeLengths[j] = huffmanData[j]; } // TODO: Pooling? short[] huffmanValues = new short[codeLengthSum]; - using (var values = new Buffer(256)) + using (var values = new Buffer(codeLengthSum)) { this.InputStream.Read(values.Array, 0, codeLengthSum); for (int j = 0; j < codeLengthSum; j++) { - huffmanValues[j] = values[o++]; + huffmanValues[j] = values[j]; } - } - i += 17 + codeLengthSum; + i += 17 + codeLengthSum; - this.BuildHuffmanTable( - huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, - huffmanTableSpec & 15, - codeLengths, - huffmanValues); + this.BuildHuffmanTable( + huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, + huffmanTableSpec & 15, + codeLengths, + huffmanValues); + } } } } private void BuildHuffmanTable(HuffmanTables tables, int index, byte[] codeLengths, short[] values) { + // (╯°□°)╯︵ ┻━┻ Everything up to here is going well. I can't match the JavaScript now though. int length = 16; while (length > 0 && codeLengths[length - 1] == 0) { length--; } - Span tableSpan = tables.Tables.GetRowSpan(index); + var code = new Queue(); + code.Enqueue(new HuffmanBranch(new List())); + HuffmanBranch p = code.Peek(); + p.Children = new List(); + HuffmanBranch q; + int k = 0; + try + { + for (int i = 0; i < length; i++) + { + for (int j = 0; j < codeLengths[i]; j++) + { + p = code.Dequeue(); + p.Children.Add(new HuffmanBranch(values[k])); + while (p.Index > 0) + { + p = code.Dequeue(); + } + + p.Index++; + code.Enqueue(p); + while (code.Count <= i) + { + q = new HuffmanBranch(new List()); + code.Enqueue(q); + p.Children.Add(new HuffmanBranch(q.Children)); + p = q; + } + + k++; + } + + if (i + 1 < length) + { + // p here points to last code + q = new HuffmanBranch(new List()); + code.Enqueue(q); + p.Children.Add(new HuffmanBranch(q.Children)); + p = q; + } + } + + Span tableSpan = tables.Tables.GetRowSpan(index); + + List result = code.Peek().Children; + for (int i = 0; i < result.Count; i++) + { + tableSpan[i] = result[i]; + } + } + catch (Exception e) + { + throw; + } } /// From 0ea7a6f687a818e17fa9c6b1d5f8937bfb76abea Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 20 Jun 2017 12:13:11 +1000 Subject: [PATCH 08/43] Can now build huffman tables --- .../Common/Extensions/ListExtensions.cs | 48 +++++++ .../Jpeg/Port/Components/HuffmanBranch.cs | 55 ++++++++ .../Jpeg/Port/Components/HuffmanTables.cs | 67 +++++---- src/ImageSharp/Formats/Jpeg/Port/Huffman.cs | 6 - .../Formats/Jpeg/Port/JpegDecoderCore.cs | 130 ++++++++---------- src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs | 33 ----- 6 files changed, 196 insertions(+), 143 deletions(-) create mode 100644 src/ImageSharp/Common/Extensions/ListExtensions.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs delete mode 100644 src/ImageSharp/Formats/Jpeg/Port/Huffman.cs delete mode 100644 src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs diff --git a/src/ImageSharp/Common/Extensions/ListExtensions.cs b/src/ImageSharp/Common/Extensions/ListExtensions.cs new file mode 100644 index 000000000..752f7ef21 --- /dev/null +++ b/src/ImageSharp/Common/Extensions/ListExtensions.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Common.Extensions +{ + using System.Collections.Generic; + + /// + /// Encapsulates a series of time saving extension methods to the class. + /// + internal static class ListExtensions + { + /// + /// Inserts an item at the given index automatically expanding the capacity if required. + /// + /// The type of object within the list + /// The list + /// The index + /// The item to insert + public static void SafeInsert(this List list, int index, T item) + { + if (index >= list.Count) + { + list.Add(item); + } + else + { + list[index] = item; + } + } + + /// + /// Removes the last element from a list and returns that element. This method changes the length of the list. + /// + /// The type of object within the list + /// The list + /// The last element in the specified sequence. + public static T Pop(this List list) + { + int last = list.Count - 1; + T item = list[last]; + list.RemoveAt(last); + return item; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs new file mode 100644 index 000000000..0f0a9b540 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System.Collections.Generic; + + /// + /// Represents a branch in the huffman tree + /// + internal struct HuffmanBranch + { + /// + /// The index + /// + public int Index; + + /// + /// The value + /// + public short Value; + + /// + /// The children + /// + public List Children; + + /// + /// Initializes a new instance of the struct. + /// + /// The value + public HuffmanBranch(short value) + : this(value, new List()) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The branch children + public HuffmanBranch(List children) + : this((short)0, children) + { + } + + private HuffmanBranch(short value, List children) + { + this.Index = 0; + this.Value = value; + this.Children = children; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs index 5fcc2007b..08c37bcbc 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -1,43 +1,50 @@ -namespace ImageSharp.Formats.Jpeg.Port.Components +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components { using System.Collections.Generic; - - using ImageSharp.Memory; + using System.Runtime.CompilerServices; /// /// Defines a pair of huffman tables /// internal class HuffmanTables { - /// - /// Gets or sets the quantization tables. - /// - public Fast2DArray Tables { get; set; } = new Fast2DArray(256, 2); - } - - internal struct HuffmanBranch - { - public HuffmanBranch(short value) - : this(value, new List()) - { - } + private List first = new List(); - public HuffmanBranch(List children) - : this(0, children) - { - } + private List second = new List(); - private HuffmanBranch(short value, List children) + /// + /// Gets or sets the table at the given index. + /// + /// The index + /// The + public List this[int index] { - this.Index = 0; - this.Value = value; - this.Children = children; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (index == 0) + { + return this.first; + } + + return this.second; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + if (index == 0) + { + this.first = value; + } + + this.second = value; + } } - - public int Index; - - public short Value; - - public List Children; } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Huffman.cs b/src/ImageSharp/Formats/Jpeg/Port/Huffman.cs deleted file mode 100644 index 75b6dc562..000000000 --- a/src/ImageSharp/Formats/Jpeg/Port/Huffman.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ImageSharp.Formats.Jpeg.Port -{ - class Huffman - { - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index db25ba845..95a83b086 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -8,8 +8,8 @@ namespace ImageSharp.Formats.Jpeg.Port using System; using System.Collections.Generic; using System.IO; - using System.Linq; + using ImageSharp.Common.Extensions; using ImageSharp.Formats.Jpeg.Port.Components; using ImageSharp.Memory; using ImageSharp.PixelFormats; @@ -50,11 +50,6 @@ namespace ImageSharp.Formats.Jpeg.Port /// private JFif jFif; - /// - /// Whether the image has a EXIF header - /// - private bool isExif; - /// /// Initializes a new instance of the class. /// @@ -345,108 +340,96 @@ namespace ImageSharp.Formats.Jpeg.Port throw new ImageFormatException("DHT has wrong length"); } - using (var huffmanData = new Buffer(remaining)) + using (var huffmanData = new Buffer(16)) { - this.InputStream.Skip(1); - this.InputStream.Read(huffmanData.Array, 0, remaining); - - for (int i = 0; i < remaining;) + for (int i = 2; i < remaining;) { - byte huffmanTableSpec = huffmanData[i]; - byte[] codeLengths = new byte[16]; - int codeLengthSum = 0; - - for (int j = 0; j < 16; j++) - { - codeLengthSum += codeLengths[j] = huffmanData[j]; - } + byte huffmanTableSpec = (byte)this.InputStream.ReadByte(); + this.InputStream.Read(huffmanData.Array, 0, 16); - // TODO: Pooling? - short[] huffmanValues = new short[codeLengthSum]; - using (var values = new Buffer(codeLengthSum)) + using (var codeLengths = new Buffer(16)) { - this.InputStream.Read(values.Array, 0, codeLengthSum); + int codeLengthSum = 0; - for (int j = 0; j < codeLengthSum; j++) + for (int j = 0; j < 16; j++) { - huffmanValues[j] = values[j]; + codeLengthSum += codeLengths[j] = huffmanData[j]; } - i += 17 + codeLengthSum; + using (var huffmanValues = new Buffer(codeLengthSum)) + { + this.InputStream.Read(huffmanValues.Array, 0, codeLengthSum); + + i += 17 + codeLengthSum; - this.BuildHuffmanTable( - huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, - huffmanTableSpec & 15, - codeLengths, - huffmanValues); + // Everything I can discover indicates there's a max of two table per DC AC pair though this limits the index to 16? + this.BuildHuffmanTable( + huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, + huffmanTableSpec & 15, + codeLengths.Array, + huffmanValues.Array); + } } } } } - private void BuildHuffmanTable(HuffmanTables tables, int index, byte[] codeLengths, short[] values) + /// + /// Builds the huffman tables + /// + /// The tables + /// The table index + /// The codelengths + /// The values + private void BuildHuffmanTable(HuffmanTables tables, int index, byte[] codeLengths, byte[] values) { - // (╯°□°)╯︵ ┻━┻ Everything up to here is going well. I can't match the JavaScript now though. int length = 16; while (length > 0 && codeLengths[length - 1] == 0) { length--; } - var code = new Queue(); - code.Enqueue(new HuffmanBranch(new List())); - HuffmanBranch p = code.Peek(); - p.Children = new List(); - HuffmanBranch q; + // TODO: Check the capacity here. Seems to max at 2 + var code = new List { new HuffmanBranch(new List()) }; + HuffmanBranch p = code[0]; int k = 0; - try + + for (int i = 0; i < length; i++) { - for (int i = 0; i < length; i++) + HuffmanBranch q; + for (int j = 0; j < codeLengths[i]; j++) { - for (int j = 0; j < codeLengths[i]; j++) + p = code.Pop(); + p.Children.SafeInsert(p.Index, new HuffmanBranch(values[k])); + while (p.Index > 0) { - p = code.Dequeue(); - p.Children.Add(new HuffmanBranch(values[k])); - while (p.Index > 0) - { - p = code.Dequeue(); - } - - p.Index++; - code.Enqueue(p); - while (code.Count <= i) - { - q = new HuffmanBranch(new List()); - code.Enqueue(q); - p.Children.Add(new HuffmanBranch(q.Children)); - p = q; - } - - k++; + p = code.Pop(); } - if (i + 1 < length) + p.Index++; + code.Add(p); + while (code.Count <= i) { - // p here points to last code q = new HuffmanBranch(new List()); - code.Enqueue(q); - p.Children.Add(new HuffmanBranch(q.Children)); + code.Add(q); + p.Children.SafeInsert(p.Index, new HuffmanBranch(q.Children)); p = q; } - } - Span tableSpan = tables.Tables.GetRowSpan(index); + k++; + } - List result = code.Peek().Children; - for (int i = 0; i < result.Count; i++) + if (i + 1 < length) { - tableSpan[i] = result[i]; + // p here points to last code + q = new HuffmanBranch(new List()); + code.Add(q); + p.Children.SafeInsert(p.Index, new HuffmanBranch(q.Children)); + p = q; } } - catch (Exception e) - { - throw; - } + + tables[index] = code[0].Children; } /// @@ -533,8 +516,7 @@ namespace ImageSharp.Formats.Jpeg.Port private ushort ReadUint16() { this.InputStream.Read(this.uint16Buffer, 0, 2); - ushort value = (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); - return value; + return (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); } /// diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs deleted file mode 100644 index a279339e7..000000000 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegFrame.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace ImageSharp.Formats.Jpeg.Port -{ - /// - /// Represents a jpeg frame - /// - internal class JpegFrame - { - /// - /// Gets or sets a value indicating whether the fame is extended - /// - public bool Extended { get; set; } - - /// - /// Gets or sets a value indicating whether the fame is progressive - /// - public bool Progressive { get; set; } - - /// - /// Gets or sets the precision - /// - public byte Precision { get; set; } - - /// - /// Gets or sets the number of scanlines within the frame - /// - public short Scanlines { get; set; } - - /// - /// Gets or sets the number of samples per scanline - /// - public short SamplesPerLine { get; set; } - } -} From a718bf852722d498c78568e9a1f4031eafbf50a3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 21 Jun 2017 00:54:24 +1000 Subject: [PATCH 09/43] Begin ProcessStartOfScan --- .../Formats/Jpeg/Port/Components/Component.cs | 10 +++++ .../Formats/Jpeg/Port/JpegConstants.cs | 18 ++++++++ .../Formats/Jpeg/Port/JpegDecoderCore.cs | 43 ++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs index 524e3b913..ca8744022 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs @@ -49,5 +49,15 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// Gets or sets the number of blocks per column /// public int BlocksPerColumn; + + /// + /// Gets the index for the DC Huffman table + /// + public int DCHuffmanTableId; + + /// + /// Gets the index for the AC Huffman table + /// + public int ACHuffmanTableId; } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs index 236e38f96..f26dbded5 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs @@ -155,6 +155,24 @@ namespace ImageSharp.Formats.Jpeg.Port /// public const ushort DHT = 0xFFC4; + /// + /// Define Restart Interval + /// + /// Specifies the interval between RSTn markers, in macroblocks.This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment. + /// + /// + public const ushort DRI = 0xFFDD; + + /// + /// Start of Scan + /// + /// Begins a top-to-bottom scan of the image. In baseline DCT JPEG images, there is generally a single scan. + /// Progressive DCT JPEG images usually contain multiple scans. This marker specifies which slice of data it + /// will contain, and is immediately followed by entropy-coded data. + /// + /// + public const ushort SOS = 0xFFDA; + /// /// Contains JFIF specific markers /// diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 95a83b086..9dd45f53b 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -45,6 +45,8 @@ namespace ImageSharp.Formats.Jpeg.Port private Frame frame; + private ushort resetInterval; + /// /// COntains information about the jFIF marker /// @@ -140,6 +142,14 @@ namespace ImageSharp.Formats.Jpeg.Port case JpegConstants.Markers.DHT: this.ProcessDefineHuffmanTablesMarker(remaining); break; + + case JpegConstants.Markers.DRI: + this.resetInterval = this.ReadUint16(); + break; + + case JpegConstants.Markers.SOS: + this.ProcessStartOfScan(); + break; } // Read on @@ -319,6 +329,8 @@ namespace ImageSharp.Formats.Jpeg.Port component.VerticalFactor = v; component.QuantizationIdentifier = this.temp[index + 2]; + this.frame.ComponentIds[i] = (byte)i; + // Don't assign the table yet. index += 3; } @@ -374,6 +386,35 @@ namespace ImageSharp.Formats.Jpeg.Port } } + /// + /// Processes the SOS (Start of scan marker). + /// + private void ProcessStartOfScan() + { + int selectorsCount = this.InputStream.ReadByte(); + var components = new List(); + + for (int i = 0; i < selectorsCount; i++) + { + byte componentIndex = this.frame.ComponentIds[this.InputStream.ReadByte() - 1]; + Component component = this.frame.Components[componentIndex]; + int tableSpec = this.InputStream.ReadByte(); + component.DCHuffmanTableId = tableSpec >> 4; + component.ACHuffmanTableId = tableSpec & 15; + components.Add(component); + } + + this.InputStream.Read(this.temp, 0, 3); + int spectralStart = this.temp[0]; + int spectralEnd = this.temp[1]; + int successiveApproximation = this.temp[2]; + } + + private int DecodeScan(List components, int spectralStart, int spectralEnd, int successivePrev, int successive) + { + return 0; + } + /// /// Builds the huffman tables /// @@ -555,4 +596,4 @@ namespace ImageSharp.Formats.Jpeg.Port // TODO: Thumbnail? } } -} +} \ No newline at end of file From 1629819f3cb442c206cf3b2fd68bbdb1988cf1db Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 22 Jun 2017 15:40:18 +1000 Subject: [PATCH 10/43] Can now decode a scan --- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 6 +- .../Jpeg/Port/Components/FileMarker.cs | 53 ++ .../Formats/Jpeg/Port/Components/Frame.cs | 41 +- .../{Component.cs => FrameComponent.cs} | 23 +- .../Jpeg/Port/Components/HuffmanBranch.cs | 21 +- .../Jpeg/Port/Components/HuffmanTables.cs | 6 +- .../Port/Components/QuantizationTables.cs | 42 +- .../Jpeg/Port/Components/ScanDecoder.cs | 471 ++++++++++++++++++ .../Formats/Jpeg/Port/JpegConstants.cs | 18 + .../Formats/Jpeg/Port/JpegDecoderCore.cs | 261 ++++++---- .../ImageSharp.Benchmarks/Image/DecodeJpeg.cs | 24 +- 11 files changed, 828 insertions(+), 138 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs rename src/ImageSharp/Formats/Jpeg/Port/Components/{Component.cs => FrameComponent.cs} (75%) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index e900e51ac..117edb225 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -25,8 +25,10 @@ namespace ImageSharp.Formats // { // return decoder.Decode(stream); // } - var decoder = new Jpeg.Port.JpegDecoderCore(options, configuration); - return decoder.Decode(stream); + using (var decoder = new Jpeg.Port.JpegDecoderCore(options, configuration)) + { + return decoder.Decode(stream); + } } } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs new file mode 100644 index 000000000..39adba5cd --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + /// + /// Represents a jpeg file marker + /// + internal struct FileMarker + { + /// + /// Initializes a new instance of the struct. + /// + /// The marker + /// The position within the stream + public FileMarker(ushort marker, long position) + { + this.Marker = marker; + this.Position = position; + this.Invalid = false; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The marker + /// The position within the stream + /// Whether the current marker is invalid + public FileMarker(ushort marker, long position, bool invalid) + { + this.Marker = marker; + this.Position = position; + this.Invalid = invalid; + } + + /// + /// Gets or sets a value indicating whether the current marker is invalid + /// + public bool Invalid { get; set; } + + /// + /// Gets the position of the marker within a stream + /// + public ushort Marker { get; } + + /// + /// Gets the position of the marker within a stream + /// + public long Position { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs index 7e72df8b0..97c422ca3 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs @@ -5,11 +5,15 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { + using System; + /// /// Represent a single jpeg frame /// - internal class Frame + internal class Frame : IDisposable { + private bool isDisposed; + /// /// Gets or sets a value indicating whether the frame uses the extended specification /// @@ -48,7 +52,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets or sets the frame component collection /// - public Component[] Components { get; set; } + public FrameComponent[] Components { get; set; } /// /// Gets or sets the maximum horizontal sampling factor @@ -69,5 +73,36 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// Gets or sets the number of MCU's per column /// public int McusPerColumn { get; set; } + + /// + public void Dispose() + { + this.Dispose(true); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Whether to dispose of managed objects + protected virtual void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + foreach (FrameComponent component in this.Components) + { + component.Dispose(); + } + } + + // Set large fields to null. + this.Components = null; + + this.isDisposed = true; + } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs similarity index 75% rename from src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs rename to src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs index ca8744022..18176bde7 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs @@ -1,20 +1,29 @@ -// +// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageSharp.Formats.Jpeg.Port.Components { + using System; + + using ImageSharp.Memory; + /// - /// Represents a single color component + /// Represents a single frame component /// - internal struct Component + internal struct FrameComponent : IDisposable { /// /// Gets or sets the component Id /// public byte Id; + /// + /// TODO: What does pred stand for? + /// + public int Pred; + /// /// Gets or sets the horizontal sampling factor. /// @@ -38,7 +47,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets or sets the block data /// - public short[] BlockData; + public Buffer BlockData; /// /// Gets or sets the number of blocks per line @@ -59,5 +68,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// Gets the index for the AC Huffman table /// public int ACHuffmanTableId; + + /// + public void Dispose() + { + this.BlockData?.Dispose(); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs index 0f0a9b540..d716355ad 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs @@ -5,7 +5,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { - using System.Collections.Generic; + using System.Runtime.CompilerServices; /// /// Represents a branch in the huffman tree @@ -23,32 +23,31 @@ namespace ImageSharp.Formats.Jpeg.Port.Components public short Value; /// - /// The children + /// The children. /// - public List Children; + public HuffmanBranch[] Children; /// /// Initializes a new instance of the struct. /// /// The value + [MethodImpl(MethodImplOptions.AggressiveInlining)] public HuffmanBranch(short value) - : this(value, new List()) { + this.Index = 0; + this.Value = value; + this.Children = new HuffmanBranch[2]; } /// /// Initializes a new instance of the struct. /// /// The branch children - public HuffmanBranch(List children) - : this((short)0, children) - { - } - - private HuffmanBranch(short value, List children) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HuffmanBranch(HuffmanBranch[] children) { this.Index = 0; - this.Value = value; + this.Value = -1; this.Children = children; } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs index 08c37bcbc..a040d21e7 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -13,16 +13,16 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// internal class HuffmanTables { - private List first = new List(); + private HuffmanBranch[] first; - private List second = new List(); + private HuffmanBranch[] second; /// /// Gets or sets the table at the given index. /// /// The index /// The - public List this[int index] + public HuffmanBranch[] this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs index f808eecfd..1ca31b31b 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs @@ -5,6 +5,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { + using System.Runtime.CompilerServices; + using ImageSharp.Memory; /// @@ -16,24 +18,30 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets the ZigZag scan table /// - public static byte[] DctZigZag { get; } = + public static byte[] DctZigZag { - 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 - }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + = + { + 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 + }; /// /// Gets or sets the quantization tables. diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs new file mode 100644 index 000000000..09837aef0 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -0,0 +1,471 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; +#if DEBUG + using System.Diagnostics; +#endif + using System.IO; + using System.Runtime.CompilerServices; + + /// + /// Encapsulates a decode method. TODO: This may well be a bottleneck + /// + /// The component + /// The offset + /// The DC Huffman tables + /// The AC Huffman tables + /// The input stream + internal delegate void DecodeAction(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream); + + /// + /// Provides the means to decode a spectral scan + /// + internal struct ScanDecoder + { + private int bitsData; + + private int bitsCount; + + private int specStart; + + private int specEnd; + + private int eobrun; + + private int successiveState; + + private int successiveACState; + + /// + /// Decodes the spectral scan + /// + /// The image frame + /// The input stream + /// The DC Huffman tables + /// The AC Huffman tables + /// The scan components + /// The reset interval + /// The spectral selection start + /// The spectral selection end + /// The successive approximation bit high end + /// The successive approximation bit low end + public void DecodeScan(Frame frame, Stream stream, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, FrameComponent[] components, ushort resetInterval, int spectralStart, int spectralEnd, int successivePrev, int successive) + { + this.specStart = spectralStart; + this.specEnd = spectralEnd; + this.successiveState = successive; + bool progressive = frame.Progressive; + int componentsLength = components.Length; + int mcusPerLine = frame.McusPerLine; + + // TODO: Delegate action will not be fast + DecodeAction decodeFn; + + if (progressive) + { + if (this.specStart == 0) + { + if (successivePrev == 0) + { + decodeFn = this.DecodeDCFirst; + } + else + { + decodeFn = this.DecodeDCSuccessive; + } + } + else + { + if (successivePrev == 0) + { + decodeFn = this.DecodeACFirst; + } + else + { + decodeFn = this.DecodeACSuccessive; + } + } + } + else + { + decodeFn = this.DecodeBaseline; + } + + int mcu = 0; + int mcuExpected; + if (componentsLength == 1) + { + mcuExpected = components[0].BlocksPerLine * components[0].BlocksPerColumn; + } + else + { + mcuExpected = mcusPerLine * frame.McusPerColumn; + } + + FileMarker fileMarker; + while (mcu < mcuExpected) + { + // Reset interval stuff + int mcuToRead = resetInterval > 0 ? Math.Min(mcuExpected - mcu, resetInterval) : mcuExpected; + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent c = ref components[i]; + c.Pred = 0; + } + + this.eobrun = 0; + + if (componentsLength == 1) + { + ref FrameComponent component = ref components[0]; + for (int n = 0; n < mcuToRead; n++) + { + DecodeBlock(dcHuffmanTables, acHuffmanTables, ref component, decodeFn, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + DecodeMcu(dcHuffmanTables, acHuffmanTables, ref component, decodeFn, mcusPerLine, mcu, j, k, stream); + } + } + } + + mcu++; + } + } + + // Find marker + this.bitsCount = 0; + + // TODO: We need to make sure we are not overwriting anything here. + fileMarker = JpegDecoderCore.FindNextFileMarker(stream); + + // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past + // those to attempt to find a valid marker (fixes issue4090.pdf) in original code. + if (fileMarker.Invalid) + { +#if DEBUG + Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); +#endif + } + + ushort marker = fileMarker.Marker; + if (marker <= 0xFF00) + { + throw new ImageFormatException("Marker was not found"); + } + + if (marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7) + { + // RSTx + stream.Skip(2); + } + else + { + break; + } + } + + fileMarker = JpegDecoderCore.FindNextFileMarker(stream); + + // Some images include more Scan blocks than expected, skip past those and + // attempt to find the next valid marker (fixes issue8182.pdf) in original code. + if (fileMarker.Invalid) + { +#if DEBUG + Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBlockBufferOffset(FrameComponent component, int row, int col) + { + return 64 * (((component.BlocksPerLine + 1) * row) + col); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeMcu(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, DecodeAction decode, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = (mcu / mcusPerLine) | 0; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + decode(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeBlock(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, DecodeAction decode, int mcu, Stream stream) + { + int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + decode(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadBit(Stream stream) + { + if (this.bitsCount > 0) + { + this.bitsCount--; + return (this.bitsData >> this.bitsCount) & 1; + } + + this.bitsData = stream.ReadByte(); + if (this.bitsData == 0xFF) + { + int nextByte = stream.ReadByte(); + if (nextByte > 0) + { + throw new ImageFormatException($"Unexpected marker {(this.bitsData << 8) | nextByte}"); + } + + // Unstuff 0 + } + + this.bitsCount = 7; + return this.bitsData >> 7; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private short DecodeHuffman(HuffmanBranch[] tree, Stream stream) + { + HuffmanBranch[] node = tree; + while (true) + { + int index; + index = this.ReadBit(stream); + HuffmanBranch branch = node[index]; + node = branch.Children; + + if (branch.Value > -1) + { + return branch.Value; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int Receive(int length, Stream stream) + { + int n = 0; + while (length > 0) + { + n = (n << 1) | this.ReadBit(stream); + length--; + } + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReceiveAndExtend(int length, Stream stream) + { + if (length == 1) + { + return this.ReadBit(stream) == 1 ? 1 : -1; + } + + int n = this.Receive(length, stream); + if (n >= 1 << (length - 1)) + { + return n; + } + + return n + (-1 << length) + 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBaseline(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); + int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream); + component.BlockData[offset] = (short)(component.Pred += diff); + + int k = 1; + while (k < 64) + { + int rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + int s = rs & 15; + int r = rs >> 4; + + if (s == 0) + { + if (r < 15) + { + break; + } + + k += 16; + continue; + } + + k += r; + byte z = QuantizationTables.DctZigZag[k]; + short re = (short)this.ReceiveAndExtend(s, stream); + component.BlockData[offset + z] = re; + k++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeDCFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); + int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream) << this.successiveState; + component.BlockData[offset] = (short)(component.Pred += diff); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeDCSuccessive(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + component.BlockData[offset] |= (short)(this.ReadBit(stream) << this.successiveState); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeACFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + if (this.eobrun > 0) + { + this.eobrun--; + return; + } + + int k = this.specStart; + int e = this.specEnd; + while (k <= e) + { + short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + int s = rs & 15; + int r = rs >> 4; + + if (s == 0) + { + if (r < 15) + { + this.eobrun = this.Receive(r, stream) + (1 << r) - 1; + break; + } + + k += 16; + continue; + } + + k += r; + byte z = QuantizationTables.DctZigZag[k]; + component.BlockData[offset + z] = (short)(this.ReceiveAndExtend(s, stream) * (1 << this.successiveState)); + k++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeACSuccessive(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + { + int k = this.specStart; + int e = this.specEnd; + int r = 0; + while (k <= e) + { + byte z = QuantizationTables.DctZigZag[k]; + int successiveACNextValue = 0; + switch (this.successiveACState) + { + case 0: // Initial state + short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + int s = rs & 15; + r = rs >> 4; + if (s == 0) + { + if (r < 15) + { + this.eobrun = this.Receive(r, stream) + (1 << r); + this.successiveACState = 4; + } + else + { + r = 16; + this.successiveACState = 1; + } + } + else + { + if (s != 1) + { + throw new ImageFormatException("Invalid ACn encoding"); + } + + successiveACNextValue = this.ReceiveAndExtend(s, stream); + this.successiveACState = r > 0 ? 2 : 3; + } + + continue; + case 1: // Skipping r zero items + case 2: + if (component.BlockData[offset + z] > 0) + { + component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + } + else + { + r--; + if (r == 0) + { + this.successiveACState = this.successiveACState == 2 ? 3 : 0; + } + } + + break; + case 3: // Set value for a zero item + if (component.BlockData[offset + z] > 0) + { + component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + } + else + { + component.BlockData[offset + z] = (short)(successiveACNextValue << this.successiveState); + this.successiveACState = 0; + } + + break; + case 4: // Eob + if (component.BlockData[offset + z] > 0) + { + component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + } + + break; + } + + k++; + } + + if (this.successiveACState == 4) + { + this.eobrun--; + if (this.eobrun == 0) + { + this.successiveACState = 0; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs index f26dbded5..0ad8afa91 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs @@ -173,6 +173,24 @@ namespace ImageSharp.Formats.Jpeg.Port /// public const ushort SOS = 0xFFDA; + /// + /// Define First Restart + /// + /// Inserted every r macroblocks, where r is the restart interval set by a DRI marker. + /// Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7. + /// + /// + public const ushort RST0 = 0xFFD0; + + /// + /// Define Eigth Restart + /// + /// Inserted every r macroblocks, where r is the restart interval set by a DRI marker. + /// Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7. + /// + /// + public const ushort RST7 = 0xFFD7; + /// /// Contains JFIF specific markers /// diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 9dd45f53b..873d4623d 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -8,6 +8,7 @@ namespace ImageSharp.Formats.Jpeg.Port using System; using System.Collections.Generic; using System.IO; + using System.Runtime.CompilerServices; using ImageSharp.Common.Extensions; using ImageSharp.Formats.Jpeg.Port.Components; @@ -18,7 +19,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// Performs the jpeg decoding operation. /// Ported from /// - internal class JpegDecoderCore + internal class JpegDecoderCore : IDisposable { /// /// The decoder options. @@ -48,10 +49,12 @@ namespace ImageSharp.Formats.Jpeg.Port private ushort resetInterval; /// - /// COntains information about the jFIF marker + /// Contains information about the jFIF marker /// private JFif jFif; + private bool isDisposed; + /// /// Initializes a new instance of the class. /// @@ -68,6 +71,109 @@ namespace ImageSharp.Formats.Jpeg.Port /// public Stream InputStream { get; private set; } + /// + /// Finds the next file marker within the byte stream. Not used but I'm keeping it for now for testing + /// + /// The input stream + /// The + public static FileMarker FindNextFileMarkerOld(Stream stream) + { + byte[] buffer = new byte[2]; + while (true) + { + int value = stream.Read(buffer, 0, 2); + + if (value == 0) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + while (buffer[0] != JpegConstants.Markers.Prefix) + { + // 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. + buffer[0] = buffer[1]; + + value = stream.ReadByte(); + if (value == -1) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + buffer[1] = (byte)value; + } + + return new FileMarker((ushort)((buffer[0] << 8) | buffer[1]), (int)(stream.Position - 2)); + } + } + + /// + /// Finds the next file marker within the byte stream + /// + /// The input stream + /// The + public static FileMarker FindNextFileMarker(Stream stream) + { + byte[] buffer = new byte[2]; + long maxPos = stream.Length - 1; + long currentPos = stream.Position; + long newPos = currentPos; + + if (currentPos >= maxPos) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + int value = stream.Read(buffer, 0, 2); + + if (value == 0) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + ushort currentMarker = (ushort)((buffer[0] << 8) | buffer[1]); + if (currentMarker >= JpegConstants.Markers.SOF0 && currentMarker <= JpegConstants.Markers.COM) + { + return new FileMarker(currentMarker, stream.Position - 2); + } + + value = stream.Read(buffer, 0, 2); + + if (value == 0) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + ushort newMarker = (ushort)((buffer[0] << 8) | buffer[1]); + while (!(newMarker >= JpegConstants.Markers.SOF0 && newMarker <= JpegConstants.Markers.COM)) + { + if (++newPos >= maxPos) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + stream.Read(buffer, 0, 2); + newMarker = (ushort)((buffer[0] << 8) | buffer[1]); + } + + return new FileMarker(newMarker, newPos, true); + } + /// /// Decodes the image from the specified and sets /// the data to image. @@ -85,27 +191,52 @@ namespace ImageSharp.Formats.Jpeg.Port return image; } + /// + public void Dispose() + { + this.Dispose(true); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Whether to dispose of managed objects + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.frame.Dispose(); + } + + // TODO: set large fields to null. + this.isDisposed = true; + } + } + private void ParseStream() { // Check for the Start Of Image marker. - ushort fileMarker = this.ReadUint16(); - if (fileMarker != JpegConstants.Markers.SOI) + var fileMarker = new FileMarker(this.ReadUint16(), 0); + if (fileMarker.Marker != JpegConstants.Markers.SOI) { throw new ImageFormatException("Missing SOI marker."); } - fileMarker = this.ReadUint16(); + ushort marker = this.ReadUint16(); + fileMarker = new FileMarker(marker, (int)this.InputStream.Position - 2); this.quantizationTables = new QuantizationTables(); this.dcHuffmanTables = new HuffmanTables(); this.acHuffmanTables = new HuffmanTables(); - while (fileMarker != JpegConstants.Markers.EOI) + while (fileMarker.Marker != JpegConstants.Markers.EOI) { // Get the marker length int remaining = this.ReadUint16() - 2; - switch (fileMarker) + switch (fileMarker.Marker) { case JpegConstants.Markers.APP0: this.ProcessApplicationHeaderMarker(remaining); @@ -148,12 +279,12 @@ namespace ImageSharp.Formats.Jpeg.Port break; case JpegConstants.Markers.SOS: - this.ProcessStartOfScan(); + this.ProcessStartOfScanMarker(); break; } // Read on - fileMarker = this.FindNextFileMarker(); + fileMarker = FindNextFileMarker(this.InputStream); } } @@ -281,7 +412,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// The remaining bytes in the segment block. /// The current frame marker. - private void ProcessStartOfFrameMarker(int remaining, ushort frameMarker) + private void ProcessStartOfFrameMarker(int remaining, FileMarker frameMarker) { if (this.frame != null) { @@ -292,8 +423,8 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame = new Frame { - Extended = frameMarker == JpegConstants.Markers.SOF1, - Progressive = frameMarker == JpegConstants.Markers.SOF2, + Extended = frameMarker.Marker == JpegConstants.Markers.SOF1, + Progressive = frameMarker.Marker == JpegConstants.Markers.SOF2, Precision = this.temp[0], Scanlines = (short)((this.temp[1] << 8) | this.temp[2]), SamplesPerLine = (short)((this.temp[3] << 8) | this.temp[4]), @@ -304,9 +435,9 @@ namespace ImageSharp.Formats.Jpeg.Port int maxV = 0; int index = 6; - // TODO: Pool this. + // No need to pool this. They max out at 4 this.frame.ComponentIds = new byte[this.frame.ComponentCount]; - this.frame.Components = new Component[this.frame.ComponentCount]; + this.frame.Components = new FrameComponent[this.frame.ComponentCount]; for (int i = 0; i < this.frame.ComponentCount; i++) { @@ -389,30 +520,36 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Processes the SOS (Start of scan marker). /// - private void ProcessStartOfScan() + private void ProcessStartOfScanMarker() { int selectorsCount = this.InputStream.ReadByte(); - var components = new List(); - for (int i = 0; i < selectorsCount; i++) { byte componentIndex = this.frame.ComponentIds[this.InputStream.ReadByte() - 1]; - Component component = this.frame.Components[componentIndex]; + ref FrameComponent component = ref this.frame.Components[componentIndex]; int tableSpec = this.InputStream.ReadByte(); component.DCHuffmanTableId = tableSpec >> 4; component.ACHuffmanTableId = tableSpec & 15; - components.Add(component); } this.InputStream.Read(this.temp, 0, 3); + int spectralStart = this.temp[0]; int spectralEnd = this.temp[1]; int successiveApproximation = this.temp[2]; - } - - private int DecodeScan(List components, int spectralStart, int spectralEnd, int successivePrev, int successive) - { - return 0; + var scanDecoder = default(ScanDecoder); + + scanDecoder.DecodeScan( + this.frame, + this.InputStream, + this.dcHuffmanTables, + this.acHuffmanTables, + this.frame.Components, + this.resetInterval, + spectralStart, + spectralEnd, + successiveApproximation >> 4, + successiveApproximation & 15); } /// @@ -430,8 +567,8 @@ namespace ImageSharp.Formats.Jpeg.Port length--; } - // TODO: Check the capacity here. Seems to max at 2 - var code = new List { new HuffmanBranch(new List()) }; + // TODO: Check the branch children capacity here. Seems to max at 2 + var code = new List { new HuffmanBranch(-1) }; HuffmanBranch p = code[0]; int k = 0; @@ -441,7 +578,7 @@ namespace ImageSharp.Formats.Jpeg.Port for (int j = 0; j < codeLengths[i]; j++) { p = code.Pop(); - p.Children.SafeInsert(p.Index, new HuffmanBranch(values[k])); + p.Children[p.Index] = new HuffmanBranch(values[k]); while (p.Index > 0) { p = code.Pop(); @@ -451,9 +588,9 @@ namespace ImageSharp.Formats.Jpeg.Port code.Add(p); while (code.Count <= i) { - q = new HuffmanBranch(new List()); + q = new HuffmanBranch(-1); code.Add(q); - p.Children.SafeInsert(p.Index, new HuffmanBranch(q.Children)); + p.Children[p.Index] = new HuffmanBranch(q.Children); p = q; } @@ -463,9 +600,9 @@ namespace ImageSharp.Formats.Jpeg.Port if (i + 1 < length) { // p here points to last code - q = new HuffmanBranch(new List()); + q = new HuffmanBranch(-1); code.Add(q); - p.Children.SafeInsert(p.Index, new HuffmanBranch(q.Children)); + p.Children[p.Index] = new HuffmanBranch(q.Children); p = q; } } @@ -478,21 +615,21 @@ namespace ImageSharp.Formats.Jpeg.Port /// private void PrepareComponents() { - int mcusPerLine = this.frame.SamplesPerLine / 8 / this.frame.MaxHorizontalFactor; - int mcusPerColumn = this.frame.Scanlines / 8 / this.frame.MaxVerticalFactor; + int mcusPerLine = (int)Math.Ceiling(this.frame.SamplesPerLine / 8D / this.frame.MaxHorizontalFactor); + int mcusPerColumn = (int)Math.Ceiling(this.frame.Scanlines / 8D / this.frame.MaxVerticalFactor); for (int i = 0; i < this.frame.ComponentCount; i++) { ref var component = ref this.frame.Components[i]; - int blocksPerLine = this.frame.SamplesPerLine / 8 * component.HorizontalFactor / this.frame.MaxHorizontalFactor; - int blocksPerColumn = this.frame.Scanlines / 8 * component.VerticalFactor / this.frame.MaxVerticalFactor; + int blocksPerLine = (int)Math.Ceiling(Math.Ceiling(this.frame.SamplesPerLine / 8D) * component.HorizontalFactor / this.frame.MaxHorizontalFactor); + int blocksPerColumn = (int)Math.Ceiling(Math.Ceiling(this.frame.Scanlines / 8D) * component.VerticalFactor / this.frame.MaxVerticalFactor); int blocksPerLineForMcu = mcusPerLine * component.HorizontalFactor; int blocksPerColumnForMcu = mcusPerColumn * component.VerticalFactor; int blocksBufferSize = 64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1); - // TODO: Pool this - component.BlockData = new short[blocksBufferSize]; + // Pooled. Disposed via frame siposal + component.BlockData = new Buffer(blocksBufferSize); component.BlocksPerLine = blocksPerLine; component.BlocksPerColumn = blocksPerColumn; } @@ -501,59 +638,11 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame.McusPerColumn = mcusPerColumn; } - /// - /// Finds the next file marker within the byte stream - /// - /// The - private ushort FindNextFileMarker() - { - while (true) - { - int value = this.InputStream.Read(this.uint16Buffer, 0, 2); - - if (value == 0) - { - return JpegConstants.Markers.EOI; - } - - while (this.uint16Buffer[0] != JpegConstants.Markers.Prefix) - { - // 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.uint16Buffer[0] = this.uint16Buffer[1]; - - value = this.InputStream.ReadByte(); - if (value == -1) - { - return JpegConstants.Markers.EOI; - } - - this.uint16Buffer[1] = (byte)value; - } - - return (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); - } - } - /// /// Reads a from the stream advancing it by two bytes /// /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] private ushort ReadUint16() { this.InputStream.Read(this.uint16Buffer, 0, 2); diff --git a/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs b/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs index 455af48ad..56771bcf5 100644 --- a/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs @@ -23,21 +23,21 @@ namespace ImageSharp.Benchmarks.Image { if (this.jpegBytes == null) { - this.jpegBytes = File.ReadAllBytes("../ImageSharp.Tests/TestImages/Formats/Jpg/Baseline/Calliphora.jpg"); + this.jpegBytes = File.ReadAllBytes("../../../../../../../../ImageSharp.Tests/TestImages/Formats/Jpg/Baseline/Calliphora.jpg"); } } - [Benchmark(Baseline = true, Description = "System.Drawing Jpeg")] - public Size JpegSystemDrawing() - { - using (MemoryStream memoryStream = new MemoryStream(this.jpegBytes)) - { - using (Image image = Image.FromStream(memoryStream)) - { - return image.Size; - } - } - } + //[Benchmark(Baseline = true, Description = "System.Drawing Jpeg")] + //public Size JpegSystemDrawing() + //{ + // using (MemoryStream memoryStream = new MemoryStream(this.jpegBytes)) + // { + // using (Image image = Image.FromStream(memoryStream)) + // { + // return image.Size; + // } + // } + //} [Benchmark(Description = "ImageSharp Jpeg")] public CoreSize JpegCore() From 8bbc63f396e70f3d24878bdc19233f60043dd080 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 23 Jun 2017 01:04:13 +1000 Subject: [PATCH 11/43] Begin second phase of decoding --- .../Formats/Jpeg/Port/Components/Adobe.cs | 38 ++++ .../Formats/Jpeg/Port/Components/Component.cs | 48 +++++ .../Jpeg/Port/Components/ComponentBlocks.cs | 53 +++++ .../Formats/Jpeg/Port/Components/Frame.cs | 8 +- .../Jpeg/Port/Components/FrameComponent.cs | 5 - .../Formats/Jpeg/Port/Components/JFif.cs | 43 ++++ .../Formats/Jpeg/Port/JpegConstants.cs | 58 +++++- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 188 ++++++++++++++---- 8 files changed, 389 insertions(+), 52 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs new file mode 100644 index 000000000..130b7bdb3 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + /// + /// Provides information about the Adobe marker segment + /// + internal struct Adobe + { + /// + /// The DCT Encode Version + /// + public short DCTEncodeVersion; + + /// + /// 0x0 : (none) + /// Bit 15 : Encoded with Blend=1 downsampling + /// + public short APP14Flags0; + + /// + /// 0x0 : (none) + /// + public short APP14Flags1; + + /// + /// Determines the colorspace transform + /// 00 : Unknown (RGB or CMYK) + /// 01 : YCbCr + /// 02 : YCCK + /// + public byte ColorTransform; + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs new file mode 100644 index 000000000..e5ae70f1f --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + + using ImageSharp.Memory; + + /// + /// Represents a component block + /// + internal struct Component : IDisposable + { + /// + /// Gets or sets the output + /// + public Buffer Output; + + /// + /// Gets or sets the horizontal scaling factor + /// + public int ScaleX; + + /// + /// Gets or sets the vertical scaling factor + /// + public int ScaleY; + + /// + /// Gets or sets the number of blocks per line + /// + public int BlocksPerLine; + + /// + /// Gets or sets the number of blocks per column + /// + public int BlocksPerColumn; + + /// + public void Dispose() + { + this.Output?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs new file mode 100644 index 000000000..5b66ad598 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + + /// + /// Contains all the decoded component blocks + /// + internal class ComponentBlocks : IDisposable + { + private bool isDisposed; + + /// + /// Gets or sets the component blocks + /// + public Component[] Components { get; set; } + + /// + public void Dispose() + { + this.Dispose(true); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Whether to dispose of managed objects + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + if (this.Components != null) + { + for (int i = 0; i < this.Components.Length; i++) + { + this.Components[i].Dispose(); + } + } + } + + // Set large fields to null. + this.Components = null; + this.isDisposed = true; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs index 97c422ca3..20fd2e9e8 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs @@ -93,15 +93,17 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (disposing) { - foreach (FrameComponent component in this.Components) + if (this.Components != null) { - component.Dispose(); + for (int i = 0; i < this.Components.Length; i++) + { + this.Components[i].Dispose(); + } } } // Set large fields to null. this.Components = null; - this.isDisposed = true; } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs index 18176bde7..0cb9bbb1c 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs @@ -39,11 +39,6 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// public byte QuantizationIdentifier; - /// - /// Gets or sets the quantization table - /// - public short[] QuantizationTable; - /// /// Gets or sets the block data /// diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs new file mode 100644 index 000000000..7fa6c44d0 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + /// + /// Provides information about the JFIF marker segment + /// + internal struct JFif + { + /// + /// The major version + /// + public byte MajorVersion; + + /// + /// The minor version + /// + public byte MinorVersion; + + /// + /// Units for the following pixel density fields + /// 00 : No units; width:height pixel aspect ratio = Ydensity:Xdensity + /// 01 : Pixels per inch (2.54 cm) + /// 02 : Pixels per centimeter + /// + public byte DensityUnits; + + /// + /// Horizontal pixel density. Must not be zero. + /// + public short XDensity; + + /// + /// Vertical pixel density. Must not be zero. + /// + public short YDensity; + + // TODO: Thumbnail? + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs index 0ad8afa91..08ae5543d 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs @@ -1,4 +1,9 @@ -// ReSharper disable InconsistentNaming +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming namespace ImageSharp.Formats.Jpeg.Port { /// @@ -191,6 +196,11 @@ namespace ImageSharp.Formats.Jpeg.Port /// public const ushort RST7 = 0xFFD7; + /// + /// Marker prefix. Next byte is a marker. + /// + public const ushort XFF = 0xFFFF; + /// /// Contains JFIF specific markers /// @@ -216,6 +226,52 @@ namespace ImageSharp.Formats.Jpeg.Port /// public const byte Null = 0; } + + /// + /// Contains Adobe specific markers + /// + public static class Adobe + { + /// + /// Represents A in ASCII + /// + public const byte A = 0x41; + + /// + /// Represents d in ASCII + /// + public const byte D = 0x64; + + /// + /// Represents b in ASCII + /// + public const byte O = 0x6F; + + /// + /// Represents b in ASCII + /// + public const byte B = 0x62; + + /// + /// Represents e in ASCII + /// + public const byte E = 0x65; + + /// + /// The color transform is unknown.(RGB or CMYK) + /// + public const byte ColorTransformUnknown = 0; + + /// + /// The color transform is YCbCr (luminance, red chroma, blue chroma) + /// + public const byte ColorTransformYCbCr = 1; + + /// + /// The color transform is YCCK (luminance, red chroma, blue chroma, keyline) + /// + public const byte ColorTransformYcck = 2; + } } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 873d4623d..deeec34fc 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -46,13 +46,29 @@ namespace ImageSharp.Formats.Jpeg.Port private Frame frame; + private ComponentBlocks components; + private ushort resetInterval; + private int width; + + private int height; + + private int numComponents; + /// - /// Contains information about the jFIF marker + /// Contains information about the JFIF marker /// private JFif jFif; + /// + /// Contains information about the Adobe marker + /// + private Adobe adobe; + + /// + /// A value indicating whether the decoder has been disposed + /// private bool isDisposed; /// @@ -208,9 +224,12 @@ namespace ImageSharp.Formats.Jpeg.Port if (disposing) { this.frame.Dispose(); + this.components.Dispose(); } - // TODO: set large fields to null. + // Set large fields to null. + this.frame = null; + this.components = null; this.isDisposed = true; } } @@ -255,9 +274,16 @@ namespace ImageSharp.Formats.Jpeg.Port case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP12: case JpegConstants.Markers.APP13: + break; + case JpegConstants.Markers.APP14: + this.ProcessApp14Marker(remaining); + break; + case JpegConstants.Markers.APP15: case JpegConstants.Markers.COM: + + // TODO: Read data block break; case JpegConstants.Markers.DQT: @@ -275,17 +301,66 @@ namespace ImageSharp.Formats.Jpeg.Port break; case JpegConstants.Markers.DRI: - this.resetInterval = this.ReadUint16(); + this.ProcessDefineRestartIntervalMarker(remaining); break; case JpegConstants.Markers.SOS: this.ProcessStartOfScanMarker(); break; + + case JpegConstants.Markers.XFF: + if ((byte)this.InputStream.ReadByte() != 0xFF) + { + // Avoid skipping a valid marker + this.InputStream.Position -= 2; + } + else + { + // Rewind that last byte we read + this.InputStream.Position -= 1; + } + + break; + + default: + + // Skip back as it could be incorrect encoding -- last 0xFF byte of the previous + // block was eaten by the encoder + this.InputStream.Position -= 3; + this.InputStream.Read(this.temp, 0, 2); + if (this.temp[0] == 0xFF && this.temp[1] >= 0xC0 && this.temp[1] <= 0xFE) + { + // Rewind that last bytes we read + this.InputStream.Position -= 2; + } + + break; } - // Read on + // Read on. TODO: Test this on damaged images. fileMarker = FindNextFileMarker(this.InputStream); } + + this.width = this.frame.SamplesPerLine; + this.height = this.frame.Scanlines; + this.components = new ComponentBlocks { Components = new Component[this.frame.ComponentCount] }; + + for (int i = 0; i < this.components.Components.Length; i++) + { + ref var frameComponent = ref this.frame.Components[i]; + var component = new Component + { + ScaleX = frameComponent.HorizontalFactor / this.frame.MaxHorizontalFactor, + ScaleY = frameComponent.VerticalFactor / this.frame.MaxVerticalFactor, + BlocksPerLine = frameComponent.BlocksPerLine, + BlocksPerColumn = frameComponent.BlocksPerColumn + }; + + this.BuildComponentData(ref component); + this.components.Components[i] = component; + } + + this.numComponents = this.components.Components.Length; } /// @@ -322,7 +397,47 @@ namespace ImageSharp.Formats.Jpeg.Port }; } - // Skip thumbnails for now. + // TODO: thumbnail + if (remaining > 0) + { + this.InputStream.Skip(remaining); + } + } + + /// + /// Processes the application header containing the Adobe identifier + /// which stores image encoding information for DCT filters. + /// + /// The remaining bytes in the segment block. + private void ProcessApp14Marker(int remaining) + { + if (remaining < 12) + { + // Skip the application header length + this.InputStream.Skip(remaining); + return; + } + + this.InputStream.Read(this.temp, 0, 12); + remaining -= 12; + + bool isAdobe = this.temp[0] == JpegConstants.Markers.Adobe.A && + this.temp[1] == JpegConstants.Markers.Adobe.D && + this.temp[2] == JpegConstants.Markers.Adobe.O && + this.temp[3] == JpegConstants.Markers.Adobe.B && + this.temp[4] == JpegConstants.Markers.Adobe.E; + + if (isAdobe) + { + this.adobe = new Adobe + { + DCTEncodeVersion = (short)((this.temp[5] << 8) | this.temp[6]), + APP14Flags0 = (short)((this.temp[7] << 8) | this.temp[8]), + APP14Flags1 = (short)((this.temp[9] << 8) | this.temp[10]), + ColorTransform = this.temp[11] + }; + } + if (remaining > 0) { this.InputStream.Skip(remaining); @@ -439,7 +554,7 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame.ComponentIds = new byte[this.frame.ComponentCount]; this.frame.Components = new FrameComponent[this.frame.ComponentCount]; - for (int i = 0; i < this.frame.ComponentCount; i++) + for (int i = 0; i < this.frame.Components.Length; i++) { int h = this.temp[index + 1] >> 4; int v = this.temp[index + 1] & 15; @@ -462,7 +577,6 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame.ComponentIds[i] = (byte)i; - // Don't assign the table yet. index += 3; } @@ -517,6 +631,21 @@ namespace ImageSharp.Formats.Jpeg.Port } } + /// + /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in + /// macroblocks + /// + /// The remaining bytes in the segment block. + private void ProcessDefineRestartIntervalMarker(int remaining) + { + if (remaining != 2) + { + throw new ImageFormatException("DRI has wrong length"); + } + + this.resetInterval = this.ReadUint16(); + } + /// /// Processes the SOS (Start of scan marker). /// @@ -552,6 +681,15 @@ namespace ImageSharp.Formats.Jpeg.Port successiveApproximation & 15); } + /// + /// Build the data for the given component + /// + /// The component + private void BuildComponentData(ref Component component) + { + // TODO: Write this + } + /// /// Builds the huffman tables /// @@ -648,41 +786,5 @@ namespace ImageSharp.Formats.Jpeg.Port this.InputStream.Read(this.uint16Buffer, 0, 2); return (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); } - - /// - /// Provides information about the JFIF marker segment - /// - internal struct JFif - { - /// - /// The major version - /// - public byte MajorVersion; - - /// - /// The minor version - /// - public byte MinorVersion; - - /// - /// Units for the following pixel density fields - /// 00 : No units; width:height pixel aspect ratio = Ydensity:Xdensity - /// 01 : Pixels per inch (2.54 cm) - /// 02 : Pixels per centimeter - /// - public byte DensityUnits; - - /// - /// Horizontal pixel density. Must not be zero. - /// - public short XDensity; - - /// - /// Vertical pixel density. Must not be zero. - /// - public short YDensity; - - // TODO: Thumbnail? - } } } \ No newline at end of file From c1025a6ea047efdccaba9b8eddfed62b966df20e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 23 Jun 2017 13:06:52 +1000 Subject: [PATCH 12/43] Impove disposal --- .../Jpeg/Port/Components/ComponentBlocks.cs | 27 ++-------- .../Formats/Jpeg/Port/Components/Frame.cs | 33 +++--------- .../Port/Components/QuantizationTables.cs | 16 ++++-- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 53 +++++++------------ 4 files changed, 42 insertions(+), 87 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs index 5b66ad598..a72835e75 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ComponentBlocks.cs @@ -10,10 +10,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Contains all the decoded component blocks /// - internal class ComponentBlocks : IDisposable + internal sealed class ComponentBlocks : IDisposable { - private bool isDisposed; - /// /// Gets or sets the component blocks /// @@ -22,31 +20,14 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// public void Dispose() { - this.Dispose(true); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// Whether to dispose of managed objects - protected virtual void Dispose(bool disposing) - { - if (!this.isDisposed) + if (this.Components != null) { - if (disposing) + for (int i = 0; i < this.Components.Length; i++) { - if (this.Components != null) - { - for (int i = 0; i < this.Components.Length; i++) - { - this.Components[i].Dispose(); - } - } + this.Components[i].Dispose(); } - // Set large fields to null. this.Components = null; - this.isDisposed = true; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs index 20fd2e9e8..06b4bbc24 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Frame.cs @@ -10,10 +10,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Represent a single jpeg frame /// - internal class Frame : IDisposable + internal sealed class Frame : IDisposable { - private bool isDisposed; - /// /// Gets or sets a value indicating whether the frame uses the extended specification /// @@ -77,34 +75,15 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// public void Dispose() { - this.Dispose(true); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// Whether to dispose of managed objects - protected virtual void Dispose(bool disposing) - { - if (this.isDisposed) + if (this.Components != null) { - return; - } - - if (disposing) - { - if (this.Components != null) + for (int i = 0; i < this.Components.Length; i++) { - for (int i = 0; i < this.Components.Length; i++) - { - this.Components[i].Dispose(); - } + this.Components[i].Dispose(); } - } - // Set large fields to null. - this.Components = null; - this.isDisposed = true; + this.Components = null; + } } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs index 1ca31b31b..fa57a18dd 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs @@ -5,15 +5,15 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { + using System; using System.Runtime.CompilerServices; using ImageSharp.Memory; /// /// Contains the quantization tables. - /// TODO: This all needs optimizing for memory. I'm just stubbing out functionality for now. /// - internal class QuantizationTables + internal sealed class QuantizationTables : IDisposable { /// /// Gets the ZigZag scan table @@ -46,6 +46,16 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets or sets the quantization tables. /// - public Fast2DArray Tables { get; set; } = new Fast2DArray(64, 4); + public Buffer2D Tables { get; set; } + + /// + public void Dispose() + { + if (this.Tables != null) + { + this.Tables.Dispose(); + this.Tables = null; + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index deeec34fc..8d3cec616 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -19,7 +19,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// Performs the jpeg decoding operation. /// Ported from /// - internal class JpegDecoderCore : IDisposable + internal sealed class JpegDecoderCore : IDisposable { /// /// The decoder options. @@ -66,11 +66,6 @@ namespace ImageSharp.Formats.Jpeg.Port /// private Adobe adobe; - /// - /// A value indicating whether the decoder has been disposed - /// - private bool isDisposed; - /// /// Initializes a new instance of the class. /// @@ -210,28 +205,14 @@ namespace ImageSharp.Formats.Jpeg.Port /// public void Dispose() { - this.Dispose(true); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// Whether to dispose of managed objects - protected virtual void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - this.frame.Dispose(); - this.components.Dispose(); - } - - // Set large fields to null. - this.frame = null; - this.components = null; - this.isDisposed = true; - } + this.frame.Dispose(); + this.components.Dispose(); + this.quantizationTables.Dispose(); + + // Set large fields to null. + this.frame = null; + this.components = null; + this.quantizationTables = null; } private void ParseStream() @@ -253,11 +234,12 @@ namespace ImageSharp.Formats.Jpeg.Port while (fileMarker.Marker != JpegConstants.Markers.EOI) { // Get the marker length - int remaining = this.ReadUint16() - 2; + int remaining; switch (fileMarker.Marker) { case JpegConstants.Markers.APP0: + remaining = this.ReadUint16() - 2; this.ProcessApplicationHeaderMarker(remaining); break; @@ -277,6 +259,7 @@ namespace ImageSharp.Formats.Jpeg.Port break; case JpegConstants.Markers.APP14: + remaining = this.ReadUint16() - 2; this.ProcessApp14Marker(remaining); break; @@ -287,24 +270,29 @@ namespace ImageSharp.Formats.Jpeg.Port break; case JpegConstants.Markers.DQT: + remaining = this.ReadUint16() - 2; this.ProcessDqtMarker(remaining); break; case JpegConstants.Markers.SOF0: case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF2: + remaining = this.ReadUint16() - 2; this.ProcessStartOfFrameMarker(remaining, fileMarker); break; case JpegConstants.Markers.DHT: + remaining = this.ReadUint16() - 2; this.ProcessDefineHuffmanTablesMarker(remaining); break; case JpegConstants.Markers.DRI: + remaining = this.ReadUint16() - 2; this.ProcessDefineRestartIntervalMarker(remaining); break; case JpegConstants.Markers.SOS: + this.InputStream.Skip(2); this.ProcessStartOfScanMarker(); break; @@ -312,11 +300,6 @@ namespace ImageSharp.Formats.Jpeg.Port if ((byte)this.InputStream.ReadByte() != 0xFF) { // Avoid skipping a valid marker - this.InputStream.Position -= 2; - } - else - { - // Rewind that last byte we read this.InputStream.Position -= 1; } @@ -453,6 +436,8 @@ namespace ImageSharp.Formats.Jpeg.Port /// private void ProcessDqtMarker(int remaining) { + // Pooled. Disposed on disposal of decoder + this.quantizationTables.Tables = new Buffer2D(64, 4); while (remaining > 0) { bool done = false; From 549e61f2f4a49563cb8993d81d57993b58bcd2e2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 24 Jun 2017 00:03:01 +1000 Subject: [PATCH 13/43] Experiment with new file marker finder --- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 2 +- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 50 +++++++++++++++++-- .../ImageSharp.Benchmarks/Image/DecodeJpeg.cs | 1 + 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 117edb225..33d82ace8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -21,7 +21,7 @@ namespace ImageSharp.Formats { Guard.NotNull(stream, "stream"); - // using (JpegDecoderCore decoder = new JpegDecoderCore(options, configuration)) + // using (var decoder = new JpegDecoderCore(options, configuration)) // { // return decoder.Decode(stream); // } diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 8d3cec616..4bb93151a 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -185,6 +185,45 @@ namespace ImageSharp.Formats.Jpeg.Port return new FileMarker(newMarker, newPos, true); } + /// + /// Finds the next file marker within the byte stream. Used for testing. Slower as it only reads on byte at a time + /// + /// The input stream + /// The + public static FileMarker FindNextFileMarkerNew(Stream stream) + { + while (true) + { + int value = stream.ReadByte(); + + if (value == -1) + { + // We've reached the end of the stream + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + byte prefix = (byte)value; + byte suffix = JpegConstants.Markers.Prefix; + + // According to Section B.1.1.2: + // "Any marker may optionally be preceded by any number of fill bytes, which are bytes assigned code 0xFF." + while (prefix == JpegConstants.Markers.Prefix && suffix == JpegConstants.Markers.Prefix) + { + value = stream.ReadByte(); + + if (value == -1) + { + // We've reached the end of the stream + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } + + suffix = (byte)value; + } + + return new FileMarker((ushort)((prefix << 8) | suffix), (int)(stream.Position - 2)); + } + } + /// /// Decodes the image from the specified and sets /// the data to image. @@ -205,9 +244,9 @@ namespace ImageSharp.Formats.Jpeg.Port /// public void Dispose() { - this.frame.Dispose(); - this.components.Dispose(); - this.quantizationTables.Dispose(); + this.frame?.Dispose(); + this.components?.Dispose(); + this.quantizationTables?.Dispose(); // Set large fields to null. this.frame = null; @@ -315,13 +354,14 @@ namespace ImageSharp.Formats.Jpeg.Port { // Rewind that last bytes we read this.InputStream.Position -= 2; + break; } - break; + throw new ImageFormatException($"Unknown Marker {fileMarker.Marker} at {fileMarker.Position}"); } // Read on. TODO: Test this on damaged images. - fileMarker = FindNextFileMarker(this.InputStream); + fileMarker = FindNextFileMarkerNew(this.InputStream); } this.width = this.frame.SamplesPerLine; diff --git a/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs b/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs index 56771bcf5..28c7d461c 100644 --- a/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Image/DecodeJpeg.cs @@ -14,6 +14,7 @@ namespace ImageSharp.Benchmarks.Image using CoreSize = ImageSharp.Size; + [Config(typeof(Config))] public class DecodeJpeg : BenchmarkBase { private byte[] jpegBytes; From 2f501eb1a49886319744d57dae9e1af4c7a2a69f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 25 Jun 2017 22:44:07 +1000 Subject: [PATCH 14/43] Decoder now doesn't break tests --- .../Jpeg/Port/Components/HuffmanTables.cs | 18 +-- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 117 ++++++------------ tests/ImageSharp.Tests/xunit.runner.json | 3 +- 3 files changed, 45 insertions(+), 93 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs index a040d21e7..a8644d645 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -13,9 +13,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// internal class HuffmanTables { - private HuffmanBranch[] first; - - private HuffmanBranch[] second; + private readonly HuffmanBranch[][] tables = new HuffmanBranch[4][]; /// /// Gets or sets the table at the given index. @@ -27,23 +25,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (index == 0) - { - return this.first; - } - - return this.second; + return this.tables[index]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] set { - if (index == 0) - { - this.first = value; - } - - this.second = value; + this.tables[index] = value; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 4bb93151a..5d50ae535 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -90,47 +90,32 @@ namespace ImageSharp.Formats.Jpeg.Port public static FileMarker FindNextFileMarkerOld(Stream stream) { byte[] buffer = new byte[2]; - while (true) + int value = stream.Read(buffer, 0, 2); + + if (value == 0) { - int value = stream.Read(buffer, 0, 2); + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } - if (value == 0) - { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } + // According to Section B.1.1.2: + // "Any marker may optionally be preceded by any number of fill bytes, which are bytes assigned code 0xFF." + if (buffer[1] != JpegConstants.Markers.Prefix) + { + return new FileMarker((ushort)((buffer[0] << 8) | buffer[1]), (int)(stream.Position - 2)); + } - while (buffer[0] != JpegConstants.Markers.Prefix) + while (buffer[1] == JpegConstants.Markers.Prefix) + { + int suffix = stream.ReadByte(); + if (suffix == -1) { - // 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. - buffer[0] = buffer[1]; - - value = stream.ReadByte(); - if (value == -1) - { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } - - buffer[1] = (byte)value; + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); } - return new FileMarker((ushort)((buffer[0] << 8) | buffer[1]), (int)(stream.Position - 2)); + buffer[1] = (byte)value; } + + return new FileMarker((ushort)((buffer[0] << 8) | buffer[1]), (int)(stream.Position - 2)); } /// @@ -185,45 +170,6 @@ namespace ImageSharp.Formats.Jpeg.Port return new FileMarker(newMarker, newPos, true); } - /// - /// Finds the next file marker within the byte stream. Used for testing. Slower as it only reads on byte at a time - /// - /// The input stream - /// The - public static FileMarker FindNextFileMarkerNew(Stream stream) - { - while (true) - { - int value = stream.ReadByte(); - - if (value == -1) - { - // We've reached the end of the stream - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } - - byte prefix = (byte)value; - byte suffix = JpegConstants.Markers.Prefix; - - // According to Section B.1.1.2: - // "Any marker may optionally be preceded by any number of fill bytes, which are bytes assigned code 0xFF." - while (prefix == JpegConstants.Markers.Prefix && suffix == JpegConstants.Markers.Prefix) - { - value = stream.ReadByte(); - - if (value == -1) - { - // We've reached the end of the stream - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } - - suffix = (byte)value; - } - - return new FileMarker((ushort)((prefix << 8) | suffix), (int)(stream.Position - 2)); - } - } - /// /// Decodes the image from the specified and sets /// the data to image. @@ -357,11 +303,12 @@ namespace ImageSharp.Formats.Jpeg.Port break; } - throw new ImageFormatException($"Unknown Marker {fileMarker.Marker} at {fileMarker.Position}"); + // throw new ImageFormatException($"Unknown Marker {fileMarker.Marker} at {fileMarker.Position}"); + break; } // Read on. TODO: Test this on damaged images. - fileMarker = FindNextFileMarkerNew(this.InputStream); + fileMarker = FindNextFileMarkerOld(this.InputStream); } this.width = this.frame.SamplesPerLine; @@ -644,7 +591,6 @@ namespace ImageSharp.Formats.Jpeg.Port i += 17 + codeLengthSum; - // Everything I can discover indicates there's a max of two table per DC AC pair though this limits the index to 16? this.BuildHuffmanTable( huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, huffmanTableSpec & 15, @@ -679,7 +625,23 @@ namespace ImageSharp.Formats.Jpeg.Port int selectorsCount = this.InputStream.ReadByte(); for (int i = 0; i < selectorsCount; i++) { - byte componentIndex = this.frame.ComponentIds[this.InputStream.ReadByte() - 1]; + int index = -1; + int selector = this.InputStream.ReadByte(); + + foreach (byte id in this.frame.ComponentIds) + { + if (selector == id) + { + index = selector; + } + } + + if (index < 0) + { + throw new ImageFormatException("Unknown component selector"); + } + + byte componentIndex = this.frame.ComponentIds[index]; ref FrameComponent component = ref this.frame.Components[componentIndex]; int tableSpec = this.InputStream.ReadByte(); component.DCHuffmanTableId = tableSpec >> 4; @@ -717,6 +679,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Builds the huffman tables + /// TODO: This is our bottleneck. We should use a faster algorithm with a LUT. /// /// The tables /// The table index diff --git a/tests/ImageSharp.Tests/xunit.runner.json b/tests/ImageSharp.Tests/xunit.runner.json index df1c3d50d..cbaa8f432 100644 --- a/tests/ImageSharp.Tests/xunit.runner.json +++ b/tests/ImageSharp.Tests/xunit.runner.json @@ -1,3 +1,4 @@ { - "methodDisplay": "method" + "methodDisplay": "method", + "diagnosticMessages": true } \ No newline at end of file From 4a4e94d74a9acdaa0e210b3eca64b595b6047e8d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 26 Jun 2017 01:18:31 +1000 Subject: [PATCH 15/43] Fix progressive decoding --- .../Jpeg/Port/Components/ScanDecoder.cs | 98 ++++++++++--------- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 50 +++++----- 2 files changed, 79 insertions(+), 69 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 09837aef0..b0c9979d2 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -49,18 +49,31 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// The DC Huffman tables /// The AC Huffman tables /// The scan components + /// The component index within the array + /// The length of the components. Different to the array length /// The reset interval /// The spectral selection start /// The spectral selection end /// The successive approximation bit high end /// The successive approximation bit low end - public void DecodeScan(Frame frame, Stream stream, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, FrameComponent[] components, ushort resetInterval, int spectralStart, int spectralEnd, int successivePrev, int successive) + public void DecodeScan( + Frame frame, + Stream stream, + HuffmanTables dcHuffmanTables, + HuffmanTables acHuffmanTables, + FrameComponent[] components, + int componentIndex, + int componentsLength, + ushort resetInterval, + int spectralStart, + int spectralEnd, + int successivePrev, + int successive) { this.specStart = spectralStart; this.specEnd = spectralEnd; this.successiveState = successive; bool progressive = frame.Progressive; - int componentsLength = components.Length; int mcusPerLine = frame.McusPerLine; // TODO: Delegate action will not be fast @@ -100,14 +113,14 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int mcuExpected; if (componentsLength == 1) { - mcuExpected = components[0].BlocksPerLine * components[0].BlocksPerColumn; + mcuExpected = components[componentIndex].BlocksPerLine * components[componentIndex].BlocksPerColumn; } else { mcuExpected = mcusPerLine * frame.McusPerColumn; } - FileMarker fileMarker; + // FileMarker fileMarker; while (mcu < mcuExpected) { // Reset interval stuff @@ -122,7 +135,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (componentsLength == 1) { - ref FrameComponent component = ref components[0]; + ref FrameComponent component = ref components[componentIndex]; for (int n = 0; n < mcuToRead; n++) { DecodeBlock(dcHuffmanTables, acHuffmanTables, ref component, decodeFn, mcu, stream); @@ -154,45 +167,41 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // Find marker this.bitsCount = 0; - // TODO: We need to make sure we are not overwriting anything here. - fileMarker = JpegDecoderCore.FindNextFileMarker(stream); - - // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past - // those to attempt to find a valid marker (fixes issue4090.pdf) in original code. - if (fileMarker.Invalid) - { -#if DEBUG - Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); -#endif - } - - ushort marker = fileMarker.Marker; - if (marker <= 0xFF00) - { - throw new ImageFormatException("Marker was not found"); - } - - if (marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7) - { - // RSTx - stream.Skip(2); - } - else - { - break; - } + // // TODO: We need to make sure we are not overwriting anything here. + // fileMarker = JpegDecoderCore.FindNextFileMarker(stream); + // // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past + // // those to attempt to find a valid marker (fixes issue4090.pdf) in original code. + // if (fileMarker.Invalid) + // { + // #if DEBUG + // Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); + // #endif + // } + // ushort marker = fileMarker.Marker; + // if (marker <= 0xFF00) + // { + // throw new ImageFormatException("Marker was not found"); + // } + // if (marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7) + // { + // // RSTx + // stream.Skip(2); + // } + // else + // { + // break; + // } } - fileMarker = JpegDecoderCore.FindNextFileMarker(stream); - - // Some images include more Scan blocks than expected, skip past those and - // attempt to find the next valid marker (fixes issue8182.pdf) in original code. - if (fileMarker.Invalid) - { -#if DEBUG - Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); -#endif - } + // fileMarker = JpegDecoderCore.FindNextFileMarker(stream); + // // Some images include more Scan blocks than expected, skip past those and + // // attempt to find the next valid marker (fixes issue8182.pdf) in original code. + // if (fileMarker.Invalid) + // { + // #if DEBUG + // Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); + // #endif + // } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -231,7 +240,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } this.bitsData = stream.ReadByte(); - if (this.bitsData == 0xFF) + if (this.bitsData == JpegConstants.Markers.Prefix) { int nextByte = stream.ReadByte(); if (nextByte > 0) @@ -252,8 +261,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components HuffmanBranch[] node = tree; while (true) { - int index; - index = this.ReadBit(stream); + int index = this.ReadBit(stream); HuffmanBranch branch = node[index]; node = branch.Children; diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 5d50ae535..abdabcd49 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -87,35 +87,33 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// The input stream /// The - public static FileMarker FindNextFileMarkerOld(Stream stream) + public static FileMarker FindNextFileMarkerNew(Stream stream) { - byte[] buffer = new byte[2]; - int value = stream.Read(buffer, 0, 2); + byte[] marker = new byte[2]; + int value = stream.Read(marker, 0, 2); if (value == 0) { return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); } - // According to Section B.1.1.2: - // "Any marker may optionally be preceded by any number of fill bytes, which are bytes assigned code 0xFF." - if (buffer[1] != JpegConstants.Markers.Prefix) - { - return new FileMarker((ushort)((buffer[0] << 8) | buffer[1]), (int)(stream.Position - 2)); - } - - while (buffer[1] == JpegConstants.Markers.Prefix) + if (marker[0] == JpegConstants.Markers.Prefix) { - int suffix = stream.ReadByte(); - if (suffix == -1) + // According to Section B.1.1.2: + // "Any marker may optionally be preceded by any number of fill bytes, which are bytes assigned code 0xFF." + while (marker[1] == JpegConstants.Markers.Prefix) { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } + int suffix = stream.ReadByte(); + if (suffix == -1) + { + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + } - buffer[1] = (byte)value; + marker[1] = (byte)value; + } } - return new FileMarker((ushort)((buffer[0] << 8) | buffer[1]), (int)(stream.Position - 2)); + return new FileMarker((ushort)((marker[0] << 8) | marker[1]), (int)(stream.Position - 2)); } /// @@ -292,6 +290,7 @@ namespace ImageSharp.Formats.Jpeg.Port default: + // TODO: Not convinced this is required // Skip back as it could be incorrect encoding -- last 0xFF byte of the previous // block was eaten by the encoder this.InputStream.Position -= 3; @@ -308,7 +307,7 @@ namespace ImageSharp.Formats.Jpeg.Port } // Read on. TODO: Test this on damaged images. - fileMarker = FindNextFileMarkerOld(this.InputStream); + fileMarker = FindNextFileMarkerNew(this.InputStream); } this.width = this.frame.SamplesPerLine; @@ -547,7 +546,7 @@ namespace ImageSharp.Formats.Jpeg.Port component.VerticalFactor = v; component.QuantizationIdentifier = this.temp[index + 2]; - this.frame.ComponentIds[i] = (byte)i; + this.frame.ComponentIds[i] = component.Id; index += 3; } @@ -623,16 +622,18 @@ namespace ImageSharp.Formats.Jpeg.Port private void ProcessStartOfScanMarker() { int selectorsCount = this.InputStream.ReadByte(); + int index = -1; for (int i = 0; i < selectorsCount; i++) { - int index = -1; + index = -1; int selector = this.InputStream.ReadByte(); - foreach (byte id in this.frame.ComponentIds) + for (int j = 0; j < this.frame.ComponentIds.Length; j++) { + byte id = this.frame.ComponentIds[j]; if (selector == id) { - index = selector; + index = j; } } @@ -641,8 +642,7 @@ namespace ImageSharp.Formats.Jpeg.Port throw new ImageFormatException("Unknown component selector"); } - byte componentIndex = this.frame.ComponentIds[index]; - ref FrameComponent component = ref this.frame.Components[componentIndex]; + ref FrameComponent component = ref this.frame.Components[index]; int tableSpec = this.InputStream.ReadByte(); component.DCHuffmanTableId = tableSpec >> 4; component.ACHuffmanTableId = tableSpec & 15; @@ -661,6 +661,8 @@ namespace ImageSharp.Formats.Jpeg.Port this.dcHuffmanTables, this.acHuffmanTables, this.frame.Components, + index, + selectorsCount, this.resetInterval, spectralStart, spectralEnd, From 472d6ba307bfe5f420ecf54a9ea4e9fd21fc4c93 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Jun 2017 00:24:54 +1000 Subject: [PATCH 16/43] baseline decode works progressive nearly --- .../Formats/Jpeg/Port/Components/Component.cs | 2 +- .../Formats/Jpeg/Port/Components/IDCT.cs | 222 +++ .../Port/Components/QuantizationTables.cs | 8 +- .../Jpeg/Port/Components/ScanDecoder.cs | 38 +- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 53 +- .../TestImages/Formats/Jpg/jpeg.htm | 63 + .../TestImages/Formats/Jpg/jpg.js | 1205 +++++++++++++++++ 7 files changed, 1568 insertions(+), 23 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpeg.htm create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpg.js diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs index e5ae70f1f..3b462514c 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs @@ -17,7 +17,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets or sets the output /// - public Buffer Output; + public Buffer Output; /// /// Gets or sets the horizontal scaling factor diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs new file mode 100644 index 000000000..0e5a97012 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -0,0 +1,222 @@ +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using ImageSharp.Memory; + + /// + /// Performa the invers + /// + internal static class IDCT + { + private const int DctCos1 = 4017; // cos(pi/16) + private const int DctSin1 = 799; // sin(pi/16) + private const int DctCos3 = 3406; // cos(3*pi/16) + private const int DctSin3 = 2276; // sin(3*pi/16) + private const int DctCos6 = 1567; // cos(6*pi/16) + private const int DctSin6 = 3784; // sin(6*pi/16) + private const int DctSqrt2 = 5793; // sqrt(2) + private const int DctSqrt1D2 = 2896; // sqrt(2) / 2 + + /// + /// A port of Poppler's IDCT method which in turn is taken from: + /// Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz, + /// 'Practical Fast 1-D DCT Algorithms with 11 Multiplications', + /// IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989, 988-991. + /// + /// The quantization tables + /// The fram component + /// The block buffer offset + /// The computational buffer for holding temp values + public static void QuantizeAndInverse(QuantizationTables quantizationTables, ref FrameComponent component, int blockBufferOffset, Buffer computationBuffer) + { + Span qt = quantizationTables.Tables.GetRowSpan(component.QuantizationIdentifier); + Buffer blockData = component.BlockData; + int v0, v1, v2, v3, v4, v5, v6, v7; + int p0, p1, p2, p3, p4, p5, p6, p7; + int t; + + // inverse DCT on rows + for (int row = 0; row < 64; row += 8) + { + // gather block data + p0 = blockData[blockBufferOffset + row]; + p1 = blockData[blockBufferOffset + row + 1]; + p2 = blockData[blockBufferOffset + row + 2]; + p3 = blockData[blockBufferOffset + row + 3]; + p4 = blockData[blockBufferOffset + row + 4]; + p5 = blockData[blockBufferOffset + row + 5]; + p6 = blockData[blockBufferOffset + row + 6]; + p7 = blockData[blockBufferOffset + row + 7]; + + // dequant p0 + p0 *= qt[row]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + t = ((DctSqrt2 * p0) + 512) >> 10; + short st = (short)t; + computationBuffer[row] = st; + computationBuffer[row + 1] = st; + computationBuffer[row + 2] = st; + computationBuffer[row + 3] = st; + computationBuffer[row + 4] = st; + computationBuffer[row + 5] = st; + computationBuffer[row + 6] = st; + computationBuffer[row + 7] = st; + continue; + } + + // dequant p1 ... p7 + p1 *= qt[row + 1]; + p2 *= qt[row + 2]; + p3 *= qt[row + 3]; + p4 *= qt[row + 4]; + p5 *= qt[row + 5]; + p6 *= qt[row + 6]; + p7 *= qt[row + 7]; + + // stage 4 + v0 = ((DctSqrt2 * p0) + 128) >> 8; + v1 = ((DctSqrt2 * p4) + 128) >> 8; + v2 = p2; + v3 = p6; + v4 = ((DctSqrt1D2 * (p1 - p7)) + 128) >> 8; + v7 = ((DctSqrt1D2 * (p1 + p7)) + 128) >> 8; + v5 = p3 << 4; + v6 = p5 << 4; + + // stage 3 + v0 = (v0 + v1 + 1) >> 1; + v1 = v0 - v1; + t = ((v2 * DctSin6) + (v3 * DctCos6) + 128) >> 8; + v2 = ((v2 * DctCos6) - (v3 * DctSin6) + 128) >> 8; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = ((v4 * DctSin3) + (v7 * DctCos3) + 2048) >> 12; + v4 = ((v4 * DctCos3) - (v7 * DctSin3) + 2048) >> 12; + v7 = t; + t = ((v5 * DctSin1) + (v6 * DctCos1) + 2048) >> 12; + v5 = ((v5 * DctCos1) - (v6 * DctSin1) + 2048) >> 12; + v6 = t; + + // stage 1 + computationBuffer[row] = (short)(v0 + v7); + computationBuffer[row + 7] = (short)(v0 - v7); + computationBuffer[row + 1] = (short)(v1 + v6); + computationBuffer[row + 6] = (short)(v1 - v6); + computationBuffer[row + 2] = (short)(v2 + v5); + computationBuffer[row + 5] = (short)(v2 - v5); + computationBuffer[row + 3] = (short)(v3 + v4); + computationBuffer[row + 4] = (short)(v3 - v4); + } + + // inverse DCT on columns + for (int col = 0; col < 8; ++col) + { + p0 = computationBuffer[col]; + p1 = computationBuffer[col + 8]; + p2 = computationBuffer[col + 16]; + p3 = computationBuffer[col + 24]; + p4 = computationBuffer[col + 32]; + p5 = computationBuffer[col + 40]; + p6 = computationBuffer[col + 48]; + p7 = computationBuffer[col + 56]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + t = ((DctSqrt2 * p0) + 8192) >> 14; + + // convert to 8 bit + t = (t < -2040) ? 0 : (t >= 2024) ? 255 : (t + 2056) >> 4; + short st = (short)t; + + blockData[blockBufferOffset + col] = st; + blockData[blockBufferOffset + col + 8] = st; + blockData[blockBufferOffset + col + 16] = st; + blockData[blockBufferOffset + col + 24] = st; + blockData[blockBufferOffset + col + 32] = st; + blockData[blockBufferOffset + col + 40] = st; + blockData[blockBufferOffset + col + 48] = st; + blockData[blockBufferOffset + col + 56] = st; + continue; + } + + // stage 4 + v0 = ((DctSqrt2 * p0) + 2048) >> 12; + v1 = ((DctSqrt2 * p4) + 2048) >> 12; + v2 = p2; + v3 = p6; + v4 = ((DctSqrt1D2 * (p1 - p7)) + 2048) >> 12; + v7 = ((DctSqrt1D2 * (p1 + p7)) + 2048) >> 12; + v5 = p3; + v6 = p5; + + // stage 3 + // Shift v0 by 128.5 << 5 here, so we don't need to shift p0...p7 when + // converting to UInt8 range later. + v0 = ((v0 + v1 + 1) >> 1) + 4112; + v1 = v0 - v1; + t = ((v2 * DctSin6) + (v3 * DctCos6) + 2048) >> 12; + v2 = ((v2 * DctCos6) - (v3 * DctSin6) + 2048) >> 12; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = ((v4 * DctSin3) + (v7 * DctCos3) + 2048) >> 12; + v4 = ((v4 * DctCos3) - (v7 * DctSin3) + 2048) >> 12; + v7 = t; + t = ((v5 * DctSin1) + (v6 * DctCos1) + 2048) >> 12; + v5 = ((v5 * DctCos1) - (v6 * DctSin1) + 2048) >> 12; + v6 = t; + + // stage 1 + p0 = v0 + v7; + p7 = v0 - v7; + p1 = v1 + v6; + p6 = v1 - v6; + p2 = v2 + v5; + p5 = v2 - v5; + p3 = v3 + v4; + p4 = v3 - v4; + + // convert to 8-bit integers + p0 = (p0 < 16) ? 0 : (p0 >= 4080) ? 255 : p0 >> 4; + p1 = (p1 < 16) ? 0 : (p1 >= 4080) ? 255 : p1 >> 4; + p2 = (p2 < 16) ? 0 : (p2 >= 4080) ? 255 : p2 >> 4; + p3 = (p3 < 16) ? 0 : (p3 >= 4080) ? 255 : p3 >> 4; + p4 = (p4 < 16) ? 0 : (p4 >= 4080) ? 255 : p4 >> 4; + p5 = (p5 < 16) ? 0 : (p5 >= 4080) ? 255 : p5 >> 4; + p6 = (p6 < 16) ? 0 : (p6 >= 4080) ? 255 : p6 >> 4; + p7 = (p7 < 16) ? 0 : (p7 >= 4080) ? 255 : p7 >> 4; + + // store block data + blockData[blockBufferOffset + col] = (short)p0; + blockData[blockBufferOffset + col + 8] = (short)p1; + blockData[blockBufferOffset + col + 16] = (short)p2; + blockData[blockBufferOffset + col + 24] = (short)p3; + blockData[blockBufferOffset + col + 32] = (short)p4; + blockData[blockBufferOffset + col + 40] = (short)p5; + blockData[blockBufferOffset + col + 48] = (short)p6; + blockData[blockBufferOffset + col + 56] = (short)p7; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs index fa57a18dd..352dc43f2 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/QuantizationTables.cs @@ -46,7 +46,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets or sets the quantization tables. /// - public Buffer2D Tables { get; set; } + public Buffer2D Tables + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; set; + } + + = new Buffer2D(64, 4); /// public void Dispose() diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index b0c9979d2..59867006f 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -41,6 +41,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private int successiveACState; + private int successiveACNextValue; + /// /// Decodes the spectral scan /// @@ -91,6 +93,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { decodeFn = this.DecodeDCSuccessive; } + + Debug.WriteLine(successivePrev == 0 ? "decodeDCFirst" : "decodeDCSuccessive"); } else { @@ -102,6 +106,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { decodeFn = this.DecodeACSuccessive; } + + Debug.WriteLine(successivePrev == 0 ? "decodeACFirst" : "decodeACSuccessive"); } } else @@ -120,16 +126,28 @@ namespace ImageSharp.Formats.Jpeg.Port.Components mcuExpected = mcusPerLine * frame.McusPerColumn; } + Debug.WriteLine("mcuExpected = " + mcuExpected); + // FileMarker fileMarker; while (mcu < mcuExpected) { - // Reset interval stuff + // Reset interval int mcuToRead = resetInterval > 0 ? Math.Min(mcuExpected - mcu, resetInterval) : mcuExpected; - for (int i = 0; i < componentsLength; i++) + + // TODO: We might just be able to loop here. + if (componentsLength == 1) { - ref FrameComponent c = ref components[i]; + ref FrameComponent c = ref components[componentIndex]; c.Pred = 0; } + else + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent c = ref components[i]; + c.Pred = 0; + } + } this.eobrun = 0; @@ -165,8 +183,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } // Find marker - this.bitsCount = 0; - + // this.bitsCount = 0; // // TODO: We need to make sure we are not overwriting anything here. // fileMarker = JpegDecoderCore.FindNextFileMarker(stream); // // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past @@ -205,7 +222,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetBlockBufferOffset(FrameComponent component, int row, int col) + private static int GetBlockBufferOffset(ref FrameComponent component, int row, int col) { return 64 * (((component.BlocksPerLine + 1) * row) + col); } @@ -217,7 +234,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; - int offset = GetBlockBufferOffset(component, blockRow, blockCol); + int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); decode(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); } @@ -226,7 +243,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { int blockRow = (mcu / component.BlocksPerLine) | 0; int blockCol = mcu % component.BlocksPerLine; - int offset = GetBlockBufferOffset(component, blockRow, blockCol); + int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); decode(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); } @@ -394,7 +411,6 @@ namespace ImageSharp.Formats.Jpeg.Port.Components while (k <= e) { byte z = QuantizationTables.DctZigZag[k]; - int successiveACNextValue = 0; switch (this.successiveACState) { case 0: // Initial state @@ -421,7 +437,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components throw new ImageFormatException("Invalid ACn encoding"); } - successiveACNextValue = this.ReceiveAndExtend(s, stream); + this.successiveACNextValue = this.ReceiveAndExtend(s, stream); this.successiveACState = r > 0 ? 2 : 3; } @@ -449,7 +465,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } else { - component.BlockData[offset + z] = (short)(successiveACNextValue << this.successiveState); + component.BlockData[offset + z] = (short)(this.successiveACNextValue << this.successiveState); this.successiveACState = 0; } diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index abdabcd49..21c28043b 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -7,6 +7,7 @@ namespace ImageSharp.Formats.Jpeg.Port { using System; using System.Collections.Generic; + using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; @@ -198,6 +199,12 @@ namespace ImageSharp.Formats.Jpeg.Port this.quantizationTables = null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBlockBufferOffset(ref Component component, int row, int col) + { + return 64 * (((component.BlocksPerLine + 1) * row) + col); + } + private void ParseStream() { // Check for the Start Of Image marker. @@ -325,7 +332,7 @@ namespace ImageSharp.Formats.Jpeg.Port BlocksPerColumn = frameComponent.BlocksPerColumn }; - this.BuildComponentData(ref component); + this.BuildComponentData(ref component, ref frameComponent); this.components.Components[i] = component; } @@ -422,8 +429,6 @@ namespace ImageSharp.Formats.Jpeg.Port /// private void ProcessDqtMarker(int remaining) { - // Pooled. Disposed on disposal of decoder - this.quantizationTables.Tables = new Buffer2D(64, 4); while (remaining > 0) { bool done = false; @@ -622,10 +627,10 @@ namespace ImageSharp.Formats.Jpeg.Port private void ProcessStartOfScanMarker() { int selectorsCount = this.InputStream.ReadByte(); - int index = -1; + int componentIndex = -1; for (int i = 0; i < selectorsCount; i++) { - index = -1; + componentIndex = -1; int selector = this.InputStream.ReadByte(); for (int j = 0; j < this.frame.ComponentIds.Length; j++) @@ -633,16 +638,16 @@ namespace ImageSharp.Formats.Jpeg.Port byte id = this.frame.ComponentIds[j]; if (selector == id) { - index = j; + componentIndex = j; } } - if (index < 0) + if (componentIndex < 0) { throw new ImageFormatException("Unknown component selector"); } - ref FrameComponent component = ref this.frame.Components[index]; + ref FrameComponent component = ref this.frame.Components[componentIndex]; int tableSpec = this.InputStream.ReadByte(); component.DCHuffmanTableId = tableSpec >> 4; component.ACHuffmanTableId = tableSpec & 15; @@ -661,22 +666,50 @@ namespace ImageSharp.Formats.Jpeg.Port this.dcHuffmanTables, this.acHuffmanTables, this.frame.Components, - index, + componentIndex, selectorsCount, this.resetInterval, spectralStart, spectralEnd, successiveApproximation >> 4, successiveApproximation & 15); + + Debug.WriteLine("spectralStart= " + spectralStart); + Debug.WriteLine("spectralEnd= " + spectralEnd); + Debug.WriteLine("successiveApproximation= " + successiveApproximation); + Debug.WriteLine("Components after"); + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 10; j++) + { + Debug.WriteLine("component [" + i + "] : value [" + j + "] =" + this.frame.Components[i].BlockData[j] + "]"); + } + } } /// /// Build the data for the given component /// /// The component - private void BuildComponentData(ref Component component) + /// The frame component + private void BuildComponentData(ref Component component, ref FrameComponent frameComponent) { // TODO: Write this + int blocksPerLine = component.BlocksPerLine; + int blocksPerColumn = component.BlocksPerColumn; + using (var computationBuffer = Buffer.CreateClean(64)) + { + for (int blockRow = 0; blockRow < blocksPerColumn; blockRow++) + { + for (int blockCol = 0; blockCol < blocksPerLine; blockCol++) + { + int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); + IDCT.QuantizeAndInverse(this.quantizationTables, ref frameComponent, offset, computationBuffer); + } + } + } + + component.Output = frameComponent.BlockData; } /// diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpeg.htm b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpeg.htm new file mode 100644 index 000000000..72a5e448b --- /dev/null +++ b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpeg.htm @@ -0,0 +1,63 @@ + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpg.js b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpg.js new file mode 100644 index 000000000..6ebf71a69 --- /dev/null +++ b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/jpg.js @@ -0,0 +1,1205 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable no-multi-spaces */ + +// import { error, warn } from '../shared/util'; + +/** + * This code was forked from https://github.com/notmasteryet/jpgjs. + * The original version was created by GitHub user notmasteryet. + * + * - The JPEG specification can be found in the ITU CCITT Recommendation T.81 + * (www.w3.org/Graphics/JPEG/itu-t81.pdf) + * - The JFIF specification can be found in the JPEG File Interchange Format + * (www.w3.org/Graphics/JPEG/jfif3.pdf) + * - The Adobe Application-Specific JPEG markers in the + * Supporting the DCT Filters in PostScript Level 2, Technical Note #5116 + * (partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf) + */ + +var error = function(val){ + console.log(val); +} + +var warn = function(val){ + console.log(val); +} + +var JpegImage = (function JpegImageClosure() { + var dctZigZag = new Uint8Array([ + 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 + ]); + + var dctCos1 = 4017; // cos(pi/16) + var dctSin1 = 799; // sin(pi/16) + var dctCos3 = 3406; // cos(3*pi/16) + var dctSin3 = 2276; // sin(3*pi/16) + var dctCos6 = 1567; // cos(6*pi/16) + var dctSin6 = 3784; // sin(6*pi/16) + var dctSqrt2 = 5793; // sqrt(2) + var dctSqrt1d2 = 2896; // sqrt(2) / 2 + + function JpegImage() { + this.decodeTransform = null; + this.colorTransform = -1; + } + + function buildHuffmanTable(codeLengths, values) { + console.log(codeLengths); + console.log(values); + + var k = 0, code = [], i, j, length = 16; + while (length > 0 && !codeLengths[length - 1]) { + length--; + } + + code.push({ children: [], index: 0, }); + var p = code[0], q; + for (i = 0; i < length; i++) { + for (j = 0; j < codeLengths[i]; j++) { + p = code.pop(); + p.children[p.index] = values[k]; + + while (p.index > 0) { + p = code.pop(); + } + p.index++; + code.push(p); + while (code.length <= i) { + code.push(q = { children: [], index: 0, }); + p.children[p.index] = q.children; + p = q; + } + k++; + } + if (i + 1 < length) { + // p here points to last code + code.push(q = { children: [], index: 0, }); + p.children[p.index] = q.children; + p = q; + } + } + console.log(code[0].children); + console.log(k); + return code[0].children; + } + + function getBlockBufferOffset(component, row, col) { + return 64 * ((component.blocksPerLine + 1) * row + col); + } + + function decodeScan(data, offset, frame, components, resetInterval, + spectralStart, spectralEnd, successivePrev, successive) { + var mcusPerLine = frame.mcusPerLine; + var progressive = frame.progressive; + var startOffset = offset, bitsData = 0, bitsCount = 0; + + function readBit() { + if (bitsCount > 0) { + bitsCount--; + return (bitsData >> bitsCount) & 1; + } + bitsData = data[offset++]; + if (bitsData === 0xFF) { + var nextByte = data[offset++]; + if (nextByte) { + error('JPEG error: unexpected marker ' + + ((bitsData << 8) | nextByte).toString(16)); + } + // unstuff 0 + } + bitsCount = 7; + return bitsData >>> 7; + } + + function decodeHuffman(tree) { + var node = tree; + while (true) { + node = node[readBit()]; + if (typeof node === 'number') { + return node; + } + if (typeof node !== 'object') { + error('JPEG error: invalid huffman sequence'); + } + } + } + + function receive(length) { + var n = 0; + while (length > 0) { + n = (n << 1) | readBit(); + length--; + } + return n; + } + + function receiveAndExtend(length) { + if (length === 1) { + return readBit() === 1 ? 1 : -1; + } + var n = receive(length); + if (n >= 1 << (length - 1)) { + return n; + } + return n + (-1 << length) + 1; + } + + function decodeBaseline(component, offset) { + var t = decodeHuffman(component.huffmanTableDC); + var diff = t === 0 ? 0 : receiveAndExtend(t); + component.blockData[offset] = (component.pred += diff); + // console.log("component"); + // console.log(component); + + if(offset === 0){ + console.log("component at 0"); + console.log(component.blockData[offset]) + } + + var k = 1; + while (k < 64) { + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) { + break; + } + k += 16; + continue; + } + k += r; + var z = dctZigZag[k]; + component.blockData[offset + z] = receiveAndExtend(s); + k++; + } + } + + function decodeDCFirst(component, offset) { + var t = decodeHuffman(component.huffmanTableDC); + var diff = t === 0 ? 0 : (receiveAndExtend(t) << successive); + component.blockData[offset] = (component.pred += diff); + } + + function decodeDCSuccessive(component, offset) { + component.blockData[offset] |= readBit() << successive; + } + + var eobrun = 0; + function decodeACFirst(component, offset) { + if (eobrun > 0) { + eobrun--; + return; + } + var k = spectralStart, e = spectralEnd; + while (k <= e) { + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r) - 1; + break; + } + k += 16; + continue; + } + k += r; + var z = dctZigZag[k]; + component.blockData[offset + z] = + receiveAndExtend(s) * (1 << successive); + k++; + } + } + + var successiveACState = 0, successiveACNextValue; + function decodeACSuccessive(component, offset) { + var k = spectralStart; + var e = spectralEnd; + var r = 0; + var s; + var rs; + while (k <= e) { + var z = dctZigZag[k]; + switch (successiveACState) { + case 0: // initial state + rs = decodeHuffman(component.huffmanTableAC); + s = rs & 15; + r = rs >> 4; + if (s === 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r); + successiveACState = 4; + } else { + r = 16; + successiveACState = 1; + } + } else { + if (s !== 1) { + error('JPEG error: invalid ACn encoding'); + } + successiveACNextValue = receiveAndExtend(s); + successiveACState = r ? 2 : 3; + } + continue; + case 1: // skipping r zero items + case 2: + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } else { + r--; + if (r === 0) { + successiveACState = successiveACState === 2 ? 3 : 0; + } + } + break; + case 3: // set value for a zero item + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } else { + component.blockData[offset + z] = + successiveACNextValue << successive; + successiveACState = 0; + } + break; + case 4: // eob + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } + break; + } + k++; + } + if (successiveACState === 4) { + eobrun--; + if (eobrun === 0) { + successiveACState = 0; + } + } + } + + function decodeMcu(component, decode, mcu, row, col) { + var mcuRow = (mcu / mcusPerLine) | 0; + var mcuCol = mcu % mcusPerLine; + var blockRow = mcuRow * component.v + row; + var blockCol = mcuCol * component.h + col; + var offset = getBlockBufferOffset(component, blockRow, blockCol); + + // console.log("MCU Offset: " + offset); + decode(component, offset); + } + + function decodeBlock(component, decode, mcu) { + var blockRow = (mcu / component.blocksPerLine) | 0; + var blockCol = mcu % component.blocksPerLine; + var offset = getBlockBufferOffset(component, blockRow, blockCol); + decode(component, offset); + } + + var componentsLength = components.length; + var component, i, j, k, n; + var decodeFn; + if (progressive) { + if (spectralStart === 0) { + decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive; + console.log(successivePrev === 0 ? "decodeDCFirst" : "decodeDCSuccessive"); + } else { + decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive; + console.log(successivePrev === 0 ? "decodeACFirst" : "decodeACSuccessive"); + } + } else { + decodeFn = decodeBaseline; + } + + var mcu = 0, fileMarker; + var mcuExpected; + if (componentsLength === 1) { + mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn; + } else { + mcuExpected = mcusPerLine * frame.mcusPerColumn; + } + + console.log("mcuExpected = "+ mcuExpected); + + var h, v; + while (mcu < mcuExpected) { + // reset interval stuff + var mcuToRead = resetInterval ? + Math.min(mcuExpected - mcu, resetInterval) : mcuExpected; + for (i = 0; i < componentsLength; i++) { + components[i].pred = 0; + } + eobrun = 0; + + if (componentsLength === 1) { + component = components[0]; + + for (n = 0; n < mcuToRead; n++) { + decodeBlock(component, decodeFn, mcu); + mcu++; + } + } else { + for (n = 0; n < mcuToRead; n++) { + for (i = 0; i < componentsLength; i++) { + component = components[i]; + h = component.h; + v = component.v; + for (j = 0; j < v; j++) { + for (k = 0; k < h; k++) { + decodeMcu(component, decodeFn, mcu, j, k); + } + } + } + mcu++; + } + } + + // find marker + bitsCount = 0; + fileMarker = findNextFileMarker(data, offset); + // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past + // those to attempt to find a valid marker (fixes issue4090.pdf). + if (fileMarker && fileMarker.invalid) { + warn('decodeScan - unexpected MCU data, next marker is: ' + + fileMarker.invalid); + offset = fileMarker.offset; + } + var marker = fileMarker && fileMarker.marker; + if (!marker || marker <= 0xFF00) { + error('JPEG error: marker was not found'); + } + + if (marker >= 0xFFD0 && marker <= 0xFFD7) { // RSTx + offset += 2; + } else { + break; + } + } + + fileMarker = findNextFileMarker(data, offset); + // Some images include more Scan blocks than expected, skip past those and + // attempt to find the next valid marker (fixes issue8182.pdf). + if (fileMarker && fileMarker.invalid) { + warn('decodeScan - unexpected Scan data, next marker is: ' + + fileMarker.invalid); + offset = fileMarker.offset; + } + + return offset - startOffset; + } + + // A port of poppler's IDCT method which in turn is taken from: + // Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz, + // 'Practical Fast 1-D DCT Algorithms with 11 Multiplications', + // IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989, + // 988-991. + function quantizeAndInverse(component, blockBufferOffset, p) { + var qt = component.quantizationTable, blockData = component.blockData; + var v0, v1, v2, v3, v4, v5, v6, v7; + var p0, p1, p2, p3, p4, p5, p6, p7; + var t; + + if (!qt) { + error('JPEG error: missing required Quantization Table.'); + } + + // inverse DCT on rows + for (var row = 0; row < 64; row += 8) { + // gather block data + p0 = blockData[blockBufferOffset + row]; + p1 = blockData[blockBufferOffset + row + 1]; + p2 = blockData[blockBufferOffset + row + 2]; + p3 = blockData[blockBufferOffset + row + 3]; + p4 = blockData[blockBufferOffset + row + 4]; + p5 = blockData[blockBufferOffset + row + 5]; + p6 = blockData[blockBufferOffset + row + 6]; + p7 = blockData[blockBufferOffset + row + 7]; + + // dequant p0 + p0 *= qt[row]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) === 0) { + t = (dctSqrt2 * p0 + 512) >> 10; + p[row] = t; + p[row + 1] = t; + p[row + 2] = t; + p[row + 3] = t; + p[row + 4] = t; + p[row + 5] = t; + p[row + 6] = t; + p[row + 7] = t; + continue; + } + // dequant p1 ... p7 + p1 *= qt[row + 1]; + p2 *= qt[row + 2]; + p3 *= qt[row + 3]; + p4 *= qt[row + 4]; + p5 *= qt[row + 5]; + p6 *= qt[row + 6]; + p7 *= qt[row + 7]; + + // stage 4 + v0 = (dctSqrt2 * p0 + 128) >> 8; + v1 = (dctSqrt2 * p4 + 128) >> 8; + v2 = p2; + v3 = p6; + v4 = (dctSqrt1d2 * (p1 - p7) + 128) >> 8; + v7 = (dctSqrt1d2 * (p1 + p7) + 128) >> 8; + v5 = p3 << 4; + v6 = p5 << 4; + + // stage 3 + v0 = (v0 + v1 + 1) >> 1; + v1 = v0 - v1; + t = (v2 * dctSin6 + v3 * dctCos6 + 128) >> 8; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 128) >> 8; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p[row] = v0 + v7; + p[row + 7] = v0 - v7; + p[row + 1] = v1 + v6; + p[row + 6] = v1 - v6; + p[row + 2] = v2 + v5; + p[row + 5] = v2 - v5; + p[row + 3] = v3 + v4; + p[row + 4] = v3 - v4; + } + + // inverse DCT on columns + for (var col = 0; col < 8; ++col) { + p0 = p[col]; + p1 = p[col + 8]; + p2 = p[col + 16]; + p3 = p[col + 24]; + p4 = p[col + 32]; + p5 = p[col + 40]; + p6 = p[col + 48]; + p7 = p[col + 56]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) === 0) { + t = (dctSqrt2 * p0 + 8192) >> 14; + // convert to 8 bit + t = (t < -2040) ? 0 : (t >= 2024) ? 255 : (t + 2056) >> 4; + blockData[blockBufferOffset + col] = t; + blockData[blockBufferOffset + col + 8] = t; + blockData[blockBufferOffset + col + 16] = t; + blockData[blockBufferOffset + col + 24] = t; + blockData[blockBufferOffset + col + 32] = t; + blockData[blockBufferOffset + col + 40] = t; + blockData[blockBufferOffset + col + 48] = t; + blockData[blockBufferOffset + col + 56] = t; + continue; + } + + // stage 4 + v0 = (dctSqrt2 * p0 + 2048) >> 12; + v1 = (dctSqrt2 * p4 + 2048) >> 12; + v2 = p2; + v3 = p6; + v4 = (dctSqrt1d2 * (p1 - p7) + 2048) >> 12; + v7 = (dctSqrt1d2 * (p1 + p7) + 2048) >> 12; + v5 = p3; + v6 = p5; + + // stage 3 + // Shift v0 by 128.5 << 5 here, so we don't need to shift p0...p7 when + // converting to UInt8 range later. + v0 = ((v0 + v1 + 1) >> 1) + 4112; + v1 = v0 - v1; + t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 2048) >> 12; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p0 = v0 + v7; + p7 = v0 - v7; + p1 = v1 + v6; + p6 = v1 - v6; + p2 = v2 + v5; + p5 = v2 - v5; + p3 = v3 + v4; + p4 = v3 - v4; + + // convert to 8-bit integers + p0 = (p0 < 16) ? 0 : (p0 >= 4080) ? 255 : p0 >> 4; + p1 = (p1 < 16) ? 0 : (p1 >= 4080) ? 255 : p1 >> 4; + p2 = (p2 < 16) ? 0 : (p2 >= 4080) ? 255 : p2 >> 4; + p3 = (p3 < 16) ? 0 : (p3 >= 4080) ? 255 : p3 >> 4; + p4 = (p4 < 16) ? 0 : (p4 >= 4080) ? 255 : p4 >> 4; + p5 = (p5 < 16) ? 0 : (p5 >= 4080) ? 255 : p5 >> 4; + p6 = (p6 < 16) ? 0 : (p6 >= 4080) ? 255 : p6 >> 4; + p7 = (p7 < 16) ? 0 : (p7 >= 4080) ? 255 : p7 >> 4; + + // store block data + blockData[blockBufferOffset + col] = p0; + blockData[blockBufferOffset + col + 8] = p1; + blockData[blockBufferOffset + col + 16] = p2; + blockData[blockBufferOffset + col + 24] = p3; + blockData[blockBufferOffset + col + 32] = p4; + blockData[blockBufferOffset + col + 40] = p5; + blockData[blockBufferOffset + col + 48] = p6; + blockData[blockBufferOffset + col + 56] = p7; + } + } + + function buildComponentData(frame, component) { + var blocksPerLine = component.blocksPerLine; + var blocksPerColumn = component.blocksPerColumn; + var computationBuffer = new Int16Array(64); + console.log("qt"); + console.log(component.quantizationTable); + for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) { + for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) { + var offset = getBlockBufferOffset(component, blockRow, blockCol); + quantizeAndInverse(component, offset, computationBuffer); + } + } + + console.log("component.blockData"); + console.log(component.blockData); + return component.blockData; + } + + function clamp0to255(a) { + return a <= 0 ? 0 : a >= 255 ? 255 : a; + } + + function findNextFileMarker(data, currentPos, startPos) { + function peekUint16(pos) { + return (data[pos] << 8) | data[pos + 1]; + } + + var maxPos = data.length - 1; + var newPos = startPos < currentPos ? startPos : currentPos; + + if (currentPos >= maxPos) { + return null; // Don't attempt to read non-existent data and just return. + } + var currentMarker = peekUint16(currentPos); + if (currentMarker >= 0xFFC0 && currentMarker <= 0xFFFE) { + return { + invalid: null, + marker: currentMarker, + offset: currentPos, + }; + } + var newMarker = peekUint16(newPos); + while (!(newMarker >= 0xFFC0 && newMarker <= 0xFFFE)) { + if (++newPos >= maxPos) { + return null; // Don't attempt to read non-existent data and just return. + } + newMarker = peekUint16(newPos); + } + return { + invalid: currentMarker.toString(16), + marker: newMarker, + offset: newPos, + }; + } + + JpegImage.prototype = { + parse: function parse(data) { + + function readUint16() { + var value = (data[offset] << 8) | data[offset + 1]; + offset += 2; + return value; + } + + function readDataBlock() { + var length = readUint16(); + var endOffset = offset + length - 2; + + var fileMarker = findNextFileMarker(data, endOffset, offset); + if (fileMarker && fileMarker.invalid) { + warn('readDataBlock - incorrect length, next marker is: ' + + fileMarker.invalid); + endOffset = fileMarker.offset; + } + + var array = data.subarray(offset, endOffset); + offset += array.length; + return array; + } + + function prepareComponents(frame) { + var mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH); + var mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV); + for (var i = 0; i < frame.components.length; i++) { + component = frame.components[i]; + var blocksPerLine = Math.ceil(Math.ceil(frame.samplesPerLine / 8) * + component.h / frame.maxH); + var blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * + component.v / frame.maxV); + var blocksPerLineForMcu = mcusPerLine * component.h; + var blocksPerColumnForMcu = mcusPerColumn * component.v; + + var blocksBufferSize = 64 * blocksPerColumnForMcu * + (blocksPerLineForMcu + 1); + component.blockData = new Int16Array(blocksBufferSize); + component.blocksPerLine = blocksPerLine; + component.blocksPerColumn = blocksPerColumn; + } + frame.mcusPerLine = mcusPerLine; + frame.mcusPerColumn = mcusPerColumn; + } + + var offset = 0; + var jfif = null; + var adobe = null; + var frame, resetInterval; + var quantizationTables = []; + var huffmanTablesAC = [], huffmanTablesDC = []; + var fileMarker = readUint16(); + if (fileMarker !== 0xFFD8) { // SOI (Start of Image) + error('JPEG error: SOI not found'); + } + + fileMarker = readUint16(); + while (fileMarker !== 0xFFD9) { // EOI (End of image) + var i, j, l; + switch (fileMarker) { + case 0xFFE0: // APP0 (Application Specific) + case 0xFFE1: // APP1 + case 0xFFE2: // APP2 + case 0xFFE3: // APP3 + case 0xFFE4: // APP4 + case 0xFFE5: // APP5 + case 0xFFE6: // APP6 + case 0xFFE7: // APP7 + case 0xFFE8: // APP8 + case 0xFFE9: // APP9 + case 0xFFEA: // APP10 + case 0xFFEB: // APP11 + case 0xFFEC: // APP12 + case 0xFFED: // APP13 + case 0xFFEE: // APP14 + case 0xFFEF: // APP15 + case 0xFFFE: // COM (Comment) + var appData = readDataBlock(); + + if (fileMarker === 0xFFE0) { + if (appData[0] === 0x4A && appData[1] === 0x46 && + appData[2] === 0x49 && appData[3] === 0x46 && + appData[4] === 0) { // 'JFIF\x00' + jfif = { + version: { major: appData[5], minor: appData[6], }, + densityUnits: appData[7], + xDensity: (appData[8] << 8) | appData[9], + yDensity: (appData[10] << 8) | appData[11], + thumbWidth: appData[12], + thumbHeight: appData[13], + thumbData: appData.subarray(14, 14 + + 3 * appData[12] * appData[13]), + }; + } + } + // TODO APP1 - Exif + if (fileMarker === 0xFFEE) { + if (appData[0] === 0x41 && appData[1] === 0x64 && + appData[2] === 0x6F && appData[3] === 0x62 && + appData[4] === 0x65) { // 'Adobe' + adobe = { + version: (appData[5] << 8) | appData[6], + flags0: (appData[7] << 8) | appData[8], + flags1: (appData[9] << 8) | appData[10], + transformCode: appData[11], + }; + } + } + break; + + case 0xFFDB: // DQT (Define Quantization Tables) + var quantizationTablesLength = readUint16(); + var quantizationTablesEnd = quantizationTablesLength + offset - 2; + var z; + while (offset < quantizationTablesEnd) { + var quantizationTableSpec = data[offset++]; + var tableData = new Uint16Array(64); + if ((quantizationTableSpec >> 4) === 0) { // 8 bit values + for (j = 0; j < 64; j++) { + z = dctZigZag[j]; + tableData[z] = data[offset++]; + } + } else if ((quantizationTableSpec >> 4) === 1) { // 16 bit values + for (j = 0; j < 64; j++) { + z = dctZigZag[j]; + tableData[z] = readUint16(); + } + } else { + error('JPEG error: DQT - invalid table spec'); + } + quantizationTables[quantizationTableSpec & 15] = tableData; + } + break; + + case 0xFFC0: // SOF0 (Start of Frame, Baseline DCT) + case 0xFFC1: // SOF1 (Start of Frame, Extended DCT) + case 0xFFC2: // SOF2 (Start of Frame, Progressive DCT) + if (frame) { + error('JPEG error: Only single frame JPEGs supported'); + } + console.log("filemarker"); + console.log(fileMarker); + console.log(offset); + readUint16(); // skip data length + frame = {}; + frame.extended = (fileMarker === 0xFFC1); + frame.progressive = (fileMarker === 0xFFC2); + frame.precision = data[offset++]; + frame.scanLines = readUint16(); + frame.samplesPerLine = readUint16(); + frame.components = []; + frame.componentIds = {}; + var componentsCount = data[offset++], componentId; + var maxH = 0, maxV = 0; + for (i = 0; i < componentsCount; i++) { + componentId = data[offset]; + var h = data[offset + 1] >> 4; + var v = data[offset + 1] & 15; + if (maxH < h) { + maxH = h; + } + if (maxV < v) { + maxV = v; + } + var qId = data[offset + 2]; + l = frame.components.push({ + h, + v, + quantizationId: qId, + quantizationTable: null, // See comment below. + }); + frame.componentIds[componentId] = l - 1; + offset += 3; + } + + frame.maxH = maxH; + frame.maxV = maxV; + prepareComponents(frame); + break; + + case 0xFFC4: // DHT (Define Huffman Tables) + var huffmanLength = readUint16(); + for (i = 2; i < huffmanLength;) { + console.log("offset= " + offset); + var huffmanTableSpec = data[offset++]; + console.log("huffmanTableSpec= " + huffmanTableSpec); + + var codeLengths = new Uint8Array(16); + var codeLengthSum = 0; + for (j = 0; j < 16; j++, offset++) { + codeLengthSum += (codeLengths[j] = data[offset]); + } + console.log("codelengthsum = " + codeLengthSum); + console.log("offset = " + offset); + var huffmanValues = new Uint8Array(codeLengthSum); + for (j = 0; j < codeLengthSum; j++, offset++) { + huffmanValues[j] = data[offset]; + } + i += 17 + codeLengthSum; + + + console.log((huffmanTableSpec >> 4) === 0 ? "DC":"AC"); + ((huffmanTableSpec >> 4) === 0 + ? huffmanTablesDC + : huffmanTablesAC)[huffmanTableSpec & 15] = + buildHuffmanTable(codeLengths, huffmanValues); + } + break; + + case 0xFFDD: // DRI (Define Restart Interval) + readUint16(); // skip data length + resetInterval = readUint16(); + break; + + case 0xFFDA: // SOS (Start of Scan) + readUint16(); // scanLength + var selectorsCount = data[offset++]; + var components = [], component; + for (i = 0; i < selectorsCount; i++) { + var ci = data[offset++]; + console.log("ci= " + ci); + console.log("offset= " + offset); + + var componentIndex = frame.componentIds[ci]; + console.log("componentIndex= " + componentIndex); + component = frame.components[componentIndex]; + var tableSpec = data[offset++]; + component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4]; + component.huffmanTableAC = huffmanTablesAC[tableSpec & 15]; + components.push(component); + } + console.log("components= " + components); + + var spectralStart = data[offset++]; + var spectralEnd = data[offset++]; + var successiveApproximation = data[offset++]; + + console.log(frame.componentIds); + console.log("spectralStart= " + spectralStart); + console.log("spectralEnd= " + spectralEnd); + console.log("successiveApproximation= " + successiveApproximation); + // console.log("components before") + // console.log(components) + var processed = decodeScan(data, offset, + frame, components, resetInterval, + spectralStart, spectralEnd, + successiveApproximation >> 4, successiveApproximation & 15); + offset += processed; + console.log("components after"); + // console.log(frame); + for (var i = 0; i < 3; i++){ + for (var j = 0; j < 10; j++){ + console.log("component ["+ i +"] : value ["+j+"] ="+ frame.components[i].blockData[j]+"]"); + } + } + break; + + case 0xFFFF: // Fill bytes + if (data[offset] !== 0xFF) { // Avoid skipping a valid marker. + offset--; + } + break; + + default: + if (data[offset - 3] === 0xFF && + data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) { + // could be incorrect encoding -- last 0xFF byte of the previous + // block was eaten by the encoder + offset -= 3; + break; + } + + // TODO: Delete this after testing + fileMarker = 0xFFD9; + // error('JPEG error: unknown marker ' + fileMarker.toString(16)); + } + fileMarker = readUint16(); + } + + console.log("quantizationTables"); + console.log(quantizationTables); + + this.width = frame.samplesPerLine; + this.height = frame.scanLines; + this.jfif = jfif; + this.adobe = adobe; + this.components = []; + for (i = 0; i < frame.components.length; i++) { + component = frame.components[i]; + + // Prevent errors when DQT markers are placed after SOF{n} markers, + // by assigning the `quantizationTable` entry after the entire image + // has been parsed (fixes issue7406.pdf). + var quantizationTable = quantizationTables[component.quantizationId]; + if (quantizationTable) { + component.quantizationTable = quantizationTable; + } + + this.components.push({ + output: buildComponentData(frame, component), + scaleX: component.h / frame.maxH, + scaleY: component.v / frame.maxV, + blocksPerLine: component.blocksPerLine, + blocksPerColumn: component.blocksPerColumn, + }); + } + + console.log("components"); + console.log(this.components); + this.numComponents = this.components.length; + }, + + _getLinearizedBlockData: function getLinearizedBlockData(width, height) { + var scaleX = this.width / width, scaleY = this.height / height; + + var component, componentScaleX, componentScaleY, blocksPerScanline; + var x, y, i, j, k; + var index; + var offset = 0; + var output; + var numComponents = this.components.length; + var dataLength = width * height * numComponents; + var data = new Uint8Array(dataLength); + var xScaleBlockOffset = new Uint32Array(width); + var mask3LSB = 0xfffffff8; // used to clear the 3 LSBs + + for (i = 0; i < numComponents; i++) { + component = this.components[i]; + componentScaleX = component.scaleX * scaleX; + componentScaleY = component.scaleY * scaleY; + offset = i; + output = component.output; + blocksPerScanline = (component.blocksPerLine + 1) << 3; + // precalculate the xScaleBlockOffset + for (x = 0; x < width; x++) { + j = 0 | (x * componentScaleX); + xScaleBlockOffset[x] = ((j & mask3LSB) << 3) | (j & 7); + } + // linearize the blocks of the component + for (y = 0; y < height; y++) { + j = 0 | (y * componentScaleY); + index = blocksPerScanline * (j & mask3LSB) | ((j & 7) << 3); + for (x = 0; x < width; x++) { + data[offset] = output[index + xScaleBlockOffset[x]]; + offset += numComponents; + } + } + } + + // decodeTransform contains pairs of multiplier (-256..256) and additive + var transform = this.decodeTransform; + if (transform) { + for (i = 0; i < dataLength;) { + for (j = 0, k = 0; j < numComponents; j++, i++, k += 2) { + data[i] = ((data[i] * transform[k]) >> 8) + transform[k + 1]; + } + } + } + return data; + }, + + _isColorConversionNeeded: function isColorConversionNeeded() { + if (this.adobe && this.adobe.transformCode) { + // The adobe transform marker overrides any previous setting + return true; + } else if (this.numComponents === 3) { + if (!this.adobe && this.colorTransform === 0) { + // If the Adobe transform marker is not present and the image + // dictionary has a 'ColorTransform' entry, explicitly set to `0`, + // then the colours should *not* be transformed. + return false; + } + return true; + } + // `this.numComponents !== 3` + if (!this.adobe && this.colorTransform === 1) { + // If the Adobe transform marker is not present and the image + // dictionary has a 'ColorTransform' entry, explicitly set to `1`, + // then the colours should be transformed. + return true; + } + return false; + }, + + _convertYccToRgb: function convertYccToRgb(data) { + var Y, Cb, Cr; + for (var i = 0, length = data.length; i < length; i += 3) { + Y = data[i ]; + Cb = data[i + 1]; + Cr = data[i + 2]; + data[i ] = clamp0to255(Y - 179.456 + 1.402 * Cr); + data[i + 1] = clamp0to255(Y + 135.459 - 0.344 * Cb - 0.714 * Cr); + data[i + 2] = clamp0to255(Y - 226.816 + 1.772 * Cb); + } + return data; + }, + + _convertYcckToRgb: function convertYcckToRgb(data) { + var Y, Cb, Cr, k; + var offset = 0; + for (var i = 0, length = data.length; i < length; i += 4) { + Y = data[i]; + Cb = data[i + 1]; + Cr = data[i + 2]; + k = data[i + 3]; + + var r = -122.67195406894 + + Cb * (-6.60635669420364e-5 * Cb + 0.000437130475926232 * Cr - + 5.4080610064599e-5 * Y + 0.00048449797120281 * k - + 0.154362151871126) + + Cr * (-0.000957964378445773 * Cr + 0.000817076911346625 * Y - + 0.00477271405408747 * k + 1.53380253221734) + + Y * (0.000961250184130688 * Y - 0.00266257332283933 * k + + 0.48357088451265) + + k * (-0.000336197177618394 * k + 0.484791561490776); + + var g = 107.268039397724 + + Cb * (2.19927104525741e-5 * Cb - 0.000640992018297945 * Cr + + 0.000659397001245577 * Y + 0.000426105652938837 * k - + 0.176491792462875) + + Cr * (-0.000778269941513683 * Cr + 0.00130872261408275 * Y + + 0.000770482631801132 * k - 0.151051492775562) + + Y * (0.00126935368114843 * Y - 0.00265090189010898 * k + + 0.25802910206845) + + k * (-0.000318913117588328 * k - 0.213742400323665); + + var b = -20.810012546947 + + Cb * (-0.000570115196973677 * Cb - 2.63409051004589e-5 * Cr + + 0.0020741088115012 * Y - 0.00288260236853442 * k + + 0.814272968359295) + + Cr * (-1.53496057440975e-5 * Cr - 0.000132689043961446 * Y + + 0.000560833691242812 * k - 0.195152027534049) + + Y * (0.00174418132927582 * Y - 0.00255243321439347 * k + + 0.116935020465145) + + k * (-0.000343531996510555 * k + 0.24165260232407); + + data[offset++] = clamp0to255(r); + data[offset++] = clamp0to255(g); + data[offset++] = clamp0to255(b); + } + return data; + }, + + _convertYcckToCmyk: function convertYcckToCmyk(data) { + var Y, Cb, Cr; + for (var i = 0, length = data.length; i < length; i += 4) { + Y = data[i]; + Cb = data[i + 1]; + Cr = data[i + 2]; + data[i ] = clamp0to255(434.456 - Y - 1.402 * Cr); + data[i + 1] = clamp0to255(119.541 - Y + 0.344 * Cb + 0.714 * Cr); + data[i + 2] = clamp0to255(481.816 - Y - 1.772 * Cb); + // K in data[i + 3] is unchanged + } + return data; + }, + + _convertCmykToRgb: function convertCmykToRgb(data) { + var c, m, y, k; + var offset = 0; + var min = -255 * 255 * 255; + var scale = 1 / 255 / 255; + for (var i = 0, length = data.length; i < length; i += 4) { + c = data[i]; + m = data[i + 1]; + y = data[i + 2]; + k = data[i + 3]; + + var r = + c * (-4.387332384609988 * c + 54.48615194189176 * m + + 18.82290502165302 * y + 212.25662451639585 * k - + 72734.4411664936) + + m * (1.7149763477362134 * m - 5.6096736904047315 * y - + 17.873870861415444 * k - 1401.7366389350734) + + y * (-2.5217340131683033 * y - 21.248923337353073 * k + + 4465.541406466231) - + k * (21.86122147463605 * k + 48317.86113160301); + var g = + c * (8.841041422036149 * c + 60.118027045597366 * m + + 6.871425592049007 * y + 31.159100130055922 * k - + 20220.756542821975) + + m * (-15.310361306967817 * m + 17.575251261109482 * y + + 131.35250912493976 * k - 48691.05921601825) + + y * (4.444339102852739 * y + 9.8632861493405 * k - + 6341.191035517494) - + k * (20.737325471181034 * k + 47890.15695978492); + var b = + c * (0.8842522430003296 * c + 8.078677503112928 * m + + 30.89978309703729 * y - 0.23883238689178934 * k - + 3616.812083916688) + + m * (10.49593273432072 * m + 63.02378494754052 * y + + 50.606957656360734 * k - 28620.90484698408) + + y * (0.03296041114873217 * y + 115.60384449646641 * k - + 49363.43385999684) - + k * (22.33816807309886 * k + 45932.16563550634); + + data[offset++] = r >= 0 ? 255 : r <= min ? 0 : 255 + r * scale | 0; + data[offset++] = g >= 0 ? 255 : g <= min ? 0 : 255 + g * scale | 0; + data[offset++] = b >= 0 ? 255 : b <= min ? 0 : 255 + b * scale | 0; + } + return data; + }, + + getData: function getData(width, height, forceRGBoutput) { + if (this.numComponents > 4) { + error('JPEG error: Unsupported color mode'); + } + // type of data: Uint8Array(width * height * numComponents) + var data = this._getLinearizedBlockData(width, height); + + if (this.numComponents === 1 && forceRGBoutput) { + var dataLength = data.length; + var rgbData = new Uint8Array(dataLength * 3); + var offset = 0; + for (var i = 0; i < dataLength; i++) { + var grayColor = data[i]; + rgbData[offset++] = grayColor; + rgbData[offset++] = grayColor; + rgbData[offset++] = grayColor; + } + return rgbData; + } else if (this.numComponents === 3 && this._isColorConversionNeeded()) { + return this._convertYccToRgb(data); + } else if (this.numComponents === 4) { + if (this._isColorConversionNeeded()) { + if (forceRGBoutput) { + return this._convertYcckToRgb(data); + } + return this._convertYcckToCmyk(data); + } else if (forceRGBoutput) { + return this._convertCmykToRgb(data); + } + } + return data; + }, + }; + + return JpegImage; +})(); + +// export { +// JpegImage, +// }; From 28a8acac01acab6537ca36cdb87c3e9ccfa02d65 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Jun 2017 09:22:09 +1000 Subject: [PATCH 17/43] Fix progressive scan decoding --- .../Formats/Jpeg/Port/Components/Component.cs | 4 +-- .../Jpeg/Port/Components/ScanDecoder.cs | 31 ++++++++++--------- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 4 +-- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs index 3b462514c..db3170613 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs @@ -22,12 +22,12 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Gets or sets the horizontal scaling factor /// - public int ScaleX; + public float ScaleX; /// /// Gets or sets the vertical scaling factor /// - public int ScaleY; + public float ScaleY; /// /// Gets or sets the number of blocks per line diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 59867006f..2ec9ea905 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -135,20 +135,20 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int mcuToRead = resetInterval > 0 ? Math.Min(mcuExpected - mcu, resetInterval) : mcuExpected; // TODO: We might just be able to loop here. - if (componentsLength == 1) + // if (componentsLength == 1) + // { + // ref FrameComponent c = ref components[componentIndex]; + // c.Pred = 0; + // } + // else + // { + for (int i = 0; i < components.Length; i++) { - ref FrameComponent c = ref components[componentIndex]; + ref FrameComponent c = ref components[i]; c.Pred = 0; } - else - { - for (int i = 0; i < componentsLength; i++) - { - ref FrameComponent c = ref components[i]; - c.Pred = 0; - } - } + // } this.eobrun = 0; if (componentsLength == 1) @@ -280,12 +280,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { int index = this.ReadBit(stream); HuffmanBranch branch = node[index]; - node = branch.Children; if (branch.Value > -1) { return branch.Value; } + + node = branch.Children; } } @@ -438,13 +439,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } this.successiveACNextValue = this.ReceiveAndExtend(s, stream); - this.successiveACState = r > 0 ? 2 : 3; + this.successiveACState = r != 0 ? 2 : 3; } continue; case 1: // Skipping r zero items case 2: - if (component.BlockData[offset + z] > 0) + if (component.BlockData[offset + z] != 0) { component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); } @@ -459,7 +460,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components break; case 3: // Set value for a zero item - if (component.BlockData[offset + z] > 0) + if (component.BlockData[offset + z] != 0) { component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); } @@ -471,7 +472,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components break; case 4: // Eob - if (component.BlockData[offset + z] > 0) + if (component.BlockData[offset + z] != 0) { component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); } diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 21c28043b..b608b4951 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -326,8 +326,8 @@ namespace ImageSharp.Formats.Jpeg.Port ref var frameComponent = ref this.frame.Components[i]; var component = new Component { - ScaleX = frameComponent.HorizontalFactor / this.frame.MaxHorizontalFactor, - ScaleY = frameComponent.VerticalFactor / this.frame.MaxVerticalFactor, + ScaleX = frameComponent.HorizontalFactor / (float)this.frame.MaxHorizontalFactor, + ScaleY = frameComponent.VerticalFactor / (float)this.frame.MaxVerticalFactor, BlocksPerLine = frameComponent.BlocksPerLine, BlocksPerColumn = frameComponent.BlocksPerColumn }; From ca9bd3579887cda33465c860adbde27f5bacf3be Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Jun 2017 02:23:43 +1000 Subject: [PATCH 18/43] Can now decode many images --- .../Formats/Jpeg/Port/Components/Adobe.cs | 40 +- .../Jpeg/Port/Components/FileMarker.cs | 6 + .../Jpeg/Port/Components/JpegPixelArea.cs | 146 ++++++ .../Jpeg/Port/Components/ScanDecoder.cs | 496 +++++++++++++----- .../Jpeg/Port/Components/YCbCrToRgbTables.cs | 128 +++++ .../Formats/Jpeg/Port/JpegDecoderCore.cs | 301 ++++++++--- tests/ImageSharp.Tests/TestFile.cs | 19 +- .../Formats/Jpg/baseline/ycck - Copy.jpg | Bin 0 -> 611572 bytes 8 files changed, 932 insertions(+), 204 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/YCbCrToRgbTables.cs create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Jpg/baseline/ycck - Copy.jpg diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs index 130b7bdb3..6ef128ccb 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Adobe.cs @@ -6,10 +6,12 @@ // ReSharper disable InconsistentNaming namespace ImageSharp.Formats.Jpeg.Port.Components { + using System; + /// /// Provides information about the Adobe marker segment /// - internal struct Adobe + internal struct Adobe : IEquatable { /// /// The DCT Encode Version @@ -34,5 +36,39 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// 02 : YCCK /// public byte ColorTransform; + + /// + public bool Equals(Adobe other) + { + return this.DCTEncodeVersion == other.DCTEncodeVersion + && this.APP14Flags0 == other.APP14Flags0 + && this.APP14Flags1 == other.APP14Flags1 + && this.ColorTransform == other.ColorTransform; + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is Adobe && this.Equals((Adobe)obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + // TODO: Merge and use HashCodeHelpers + int hashCode = this.DCTEncodeVersion.GetHashCode(); + hashCode = (hashCode * 397) ^ this.APP14Flags0.GetHashCode(); + hashCode = (hashCode * 397) ^ this.APP14Flags1.GetHashCode(); + hashCode = (hashCode * 397) ^ this.ColorTransform.GetHashCode(); + return hashCode; + } + } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs index 39adba5cd..eaf3dafb9 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FileMarker.cs @@ -49,5 +49,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// Gets the position of the marker within a stream /// public long Position { get; } + + /// + public override string ToString() + { + return this.Marker.ToString("X"); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs new file mode 100644 index 000000000..eafc6c33c --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs @@ -0,0 +1,146 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using System.Diagnostics; + using System.Runtime.CompilerServices; + using ImageSharp.Memory; + + /// + /// Represents a section of the jpeg component data laid out in pixel order. + /// + internal struct JpegPixelArea : IDisposable + { + private readonly int imageWidth; + + private readonly int imageHeight; + + private Buffer componentData; + + private int rowStride; + + /// + /// Initializes a new instance of the struct. + /// + /// The image width + /// The image height + /// The number of components + public JpegPixelArea(int imageWidth, int imageHeight, int numberOfComponents) + { + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.Width = 0; + this.Height = 0; + this.NumberOfComponents = numberOfComponents; + this.componentData = null; + this.rowStride = 0; + } + + /// + /// Gets the number of components + /// + public int NumberOfComponents { get; } + + /// + /// Gets the width + /// + public int Width { get; private set; } + + /// + /// Gets the height + /// + public int Height { get; private set; } + + /// + /// Organsizes the decoded jpeg components into a linear array ordered by component. + /// This must be called before attempting to retrieve the data. + /// + /// The jpeg component blocks + /// The pixel area width + /// The pixel area height + public void LinearizeBlockData(ComponentBlocks components, int width, int height) + { + this.Width = width; + this.Height = height; + int numberOfComponents = this.NumberOfComponents; + this.rowStride = width * numberOfComponents; + + float scaleX = this.imageWidth / (float)width; + float scaleY = this.imageHeight / (float)height; + this.componentData = new Buffer(width * height * numberOfComponents); + const uint Mask3Lsb = 0xFFFFFFF8; // Used to clear the 3 LSBs + + using (var xScaleBlockOffset = new Buffer(width)) + { + for (int i = 0; i < numberOfComponents; i++) + { + ref Component component = ref components.Components[i]; + float componentScaleX = component.ScaleX * scaleX; + float componentScaleY = component.ScaleY * scaleY; + int offset = i; + Buffer output = component.Output; + int blocksPerScanline = (component.BlocksPerLine + 1) << 3; + + // Precalculate the xScaleBlockOffset + int j; + for (int x = 0; x < width; x++) + { + j = 0 | (int)(x * componentScaleX); + xScaleBlockOffset[x] = (int)((j & Mask3Lsb) << 3) | (j & 7); + } + + // Linearize the blocks of the component + for (int y = 0; y < height; y++) + { + j = 0 | (int)(y * componentScaleY); + int index = blocksPerScanline * (int)(j & Mask3Lsb) | ((j & 7) << 3); + for (int x = 0; x < width; x++) + { + this.componentData[offset] = (byte)output[index + xScaleBlockOffset[x]]; + offset += numberOfComponents; + } + } + } + } + } + + /// + /// Gets a representing the row 'y' beginning from the the first byte on that row. + /// + /// The y-coordinate of the pixel row. Must be greater than or equal to zero and less than the height of the pixel area. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetRowSpan(int y) + { + this.CheckCoordinates(y); + return this.componentData.Slice(y * this.rowStride, this.rowStride); + } + + /// + public void Dispose() + { + this.componentData?.Dispose(); + this.componentData = null; + } + + /// + /// Checks the coordinates to ensure they are within bounds. + /// + /// The y-coordinate of the row. Must be greater than zero and less than the height of the area. + /// + /// Thrown if the coordinates are not within the bounds of the image. + /// + [Conditional("DEBUG")] + private void CheckCoordinates(int y) + { + if (y < 0 || y >= this.Height) + { + throw new ArgumentOutOfRangeException(nameof(y), y, $"{y} is outwith the area bounds."); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 2ec9ea905..da7bb52a9 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -12,16 +12,6 @@ namespace ImageSharp.Formats.Jpeg.Port.Components using System.IO; using System.Runtime.CompilerServices; - /// - /// Encapsulates a decode method. TODO: This may well be a bottleneck - /// - /// The component - /// The offset - /// The DC Huffman tables - /// The AC Huffman tables - /// The input stream - internal delegate void DecodeAction(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream); - /// /// Provides the means to decode a spectral scan /// @@ -37,6 +27,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private int eobrun; + private int compIndex; + private int successiveState; private int successiveACState; @@ -58,7 +50,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// The spectral selection end /// The successive approximation bit high end /// The successive approximation bit low end - public void DecodeScan( + /// The representing the processed length in bytes + public long DecodeScan( Frame frame, Stream stream, HuffmanTables dcHuffmanTables, @@ -72,179 +65,438 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int successivePrev, int successive) { + this.compIndex = componentIndex; this.specStart = spectralStart; this.specEnd = spectralEnd; this.successiveState = successive; bool progressive = frame.Progressive; int mcusPerLine = frame.McusPerLine; + long startPosition = stream.Position; - // TODO: Delegate action will not be fast - DecodeAction decodeFn; + int mcu = 0; + int mcuExpected; + if (componentsLength == 1) + { + mcuExpected = components[this.compIndex].BlocksPerLine * components[this.compIndex].BlocksPerColumn; + } + else + { + mcuExpected = mcusPerLine * frame.McusPerColumn; + } - if (progressive) + FileMarker fileMarker; + while (mcu < mcuExpected) { - if (this.specStart == 0) + // Reset interval stuff + int mcuToRead = resetInterval != 0 ? Math.Min(mcuExpected - mcu, resetInterval) : mcuExpected; + for (int i = 0; i < components.Length; i++) { - if (successivePrev == 0) - { - decodeFn = this.DecodeDCFirst; - } - else - { - decodeFn = this.DecodeDCSuccessive; - } + ref FrameComponent c = ref components[i]; + c.Pred = 0; + } + + this.eobrun = 0; - Debug.WriteLine(successivePrev == 0 ? "decodeDCFirst" : "decodeDCSuccessive"); + if (!progressive) + { + this.DecodeScanBaseline(dcHuffmanTables, acHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); } else { - if (successivePrev == 0) + if (this.specStart == 0) { - decodeFn = this.DecodeACFirst; + if (successivePrev == 0) + { + this.DecodeScanDCFirst(dcHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } + else + { + this.DecodeScanDCSuccessive(components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } } else { - decodeFn = this.DecodeACSuccessive; + if (successivePrev == 0) + { + this.DecodeScanACFirst(acHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } + else + { + this.DecodeScanACSuccessive(acHuffmanTables, components, componentsLength, mcusPerLine, mcuToRead, ref mcu, stream); + } } + } + + // Find marker + this.bitsCount = 0; + long position = stream.Position; + fileMarker = JpegDecoderCore.FindNextFileMarkerNew(stream); + + // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past + // those to attempt to find a valid marker (fixes issue4090.pdf) in original code. + if (fileMarker.Invalid) + { +#if DEBUG + Debug.WriteLine($"DecodeScan - Unexpected MCU data at {stream.Position}, next marker is: {fileMarker.Marker}"); + +#endif + // stream.Position = fileMarker.Position; + } + + ushort marker = fileMarker.Marker; + + // if (marker <= 0xFF00) + // { + // throw new ImageFormatException("Marker was not found"); + // } + + // RSTn We've alread read the bytes and altered the position so no need to skip + if (marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7) + { + continue; + } - Debug.WriteLine(successivePrev == 0 ? "decodeACFirst" : "decodeACSuccessive"); + if (!fileMarker.Invalid) + { + // We've found a valid marker. + // Rewind the stream to the position of the marker and beak + stream.Position = fileMarker.Position; + break; } + + // Rewind the stream + stream.Position = position; } - else + + fileMarker = JpegDecoderCore.FindNextFileMarkerNew(stream); + + // Some images include more Scan blocks than expected, skip past those and + // attempt to find the next valid marker (fixes issue8182.pdf) in original code. + if (fileMarker.Invalid) { - decodeFn = this.DecodeBaseline; +#if DEBUG + Debug.WriteLine($"DecodeScan - Unexpected MCU data, next marker is: {fileMarker.Marker}"); +#endif + stream.Position = fileMarker.Position; } - int mcu = 0; - int mcuExpected; + return stream.Position - startPosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBlockBufferOffset(FrameComponent component, int row, int col) + { + return 64 * (((component.BlocksPerLine + 1) * row) + col); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanBaseline( + HuffmanTables dcHuffmanTables, + HuffmanTables acHuffmanTables, + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { if (componentsLength == 1) { - mcuExpected = components[componentIndex].BlocksPerLine * components[componentIndex].BlocksPerColumn; + ref FrameComponent component = ref components[this.compIndex]; + for (int n = 0; n < mcuToRead; n++) + { + this.DecodeBlockBaseline(dcHuffmanTables, acHuffmanTables, ref component, mcu, stream); + mcu++; + } } else { - mcuExpected = mcusPerLine * frame.McusPerColumn; - } + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + this.DecodeMcuBaseline(dcHuffmanTables, acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); + } + } + } - Debug.WriteLine("mcuExpected = " + mcuExpected); + mcu++; + } + } + } - // FileMarker fileMarker; - while (mcu < mcuExpected) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanDCFirst( + HuffmanTables dcHuffmanTables, + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) { - // Reset interval - int mcuToRead = resetInterval > 0 ? Math.Min(mcuExpected - mcu, resetInterval) : mcuExpected; - - // TODO: We might just be able to loop here. - // if (componentsLength == 1) - // { - // ref FrameComponent c = ref components[componentIndex]; - // c.Pred = 0; - // } - // else - // { - for (int i = 0; i < components.Length; i++) + ref FrameComponent component = ref components[this.compIndex]; + for (int n = 0; n < mcuToRead; n++) { - ref FrameComponent c = ref components[i]; - c.Pred = 0; + this.DecodeBlockDCFirst(dcHuffmanTables, ref component, mcu, stream); + mcu++; } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + this.DecodeMcuDCFirst(dcHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); + } + } + } - // } - this.eobrun = 0; + mcu++; + } + } + } - if (componentsLength == 1) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanDCSuccessive( + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) + { + ref FrameComponent component = ref components[this.compIndex]; + for (int n = 0; n < mcuToRead; n++) + { + this.DecodeBlockDCSuccessive(ref component, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) { - ref FrameComponent component = ref components[componentIndex]; - for (int n = 0; n < mcuToRead; n++) + for (int i = 0; i < componentsLength; i++) { - DecodeBlock(dcHuffmanTables, acHuffmanTables, ref component, decodeFn, mcu, stream); - mcu++; + ref FrameComponent component = ref components[i]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + this.DecodeMcuDCSuccessive(ref component, mcusPerLine, mcu, j, k, stream); + } + } } + + mcu++; } - else + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanACFirst( + HuffmanTables acHuffmanTables, + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) + { + ref FrameComponent component = ref components[this.compIndex]; + for (int n = 0; n < mcuToRead; n++) + { + this.DecodeBlockACFirst(acHuffmanTables, ref component, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) { - for (int n = 0; n < mcuToRead; n++) + for (int i = 0; i < componentsLength; i++) { - for (int i = 0; i < componentsLength; i++) + ref FrameComponent component = ref components[i]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + for (int j = 0; j < v; j++) { - ref FrameComponent component = ref components[i]; - int h = component.HorizontalFactor; - int v = component.VerticalFactor; - for (int j = 0; j < v; j++) + for (int k = 0; k < h; k++) { - for (int k = 0; k < h; k++) - { - DecodeMcu(dcHuffmanTables, acHuffmanTables, ref component, decodeFn, mcusPerLine, mcu, j, k, stream); - } + this.DecodeMcuACFirst(acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); } } - - mcu++; } + + mcu++; } + } + } - // Find marker - // this.bitsCount = 0; - // // TODO: We need to make sure we are not overwriting anything here. - // fileMarker = JpegDecoderCore.FindNextFileMarker(stream); - // // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past - // // those to attempt to find a valid marker (fixes issue4090.pdf) in original code. - // if (fileMarker.Invalid) - // { - // #if DEBUG - // Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); - // #endif - // } - // ushort marker = fileMarker.Marker; - // if (marker <= 0xFF00) - // { - // throw new ImageFormatException("Marker was not found"); - // } - // if (marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7) - // { - // // RSTx - // stream.Skip(2); - // } - // else - // { - // break; - // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeScanACSuccessive( + HuffmanTables acHuffmanTables, + FrameComponent[] components, + int componentsLength, + int mcusPerLine, + int mcuToRead, + ref int mcu, + Stream stream) + { + if (componentsLength == 1) + { + ref FrameComponent component = ref components[this.compIndex]; + for (int n = 0; n < mcuToRead; n++) + { + this.DecodeBlockACSuccessive(acHuffmanTables, ref component, mcu, stream); + mcu++; + } + } + else + { + for (int n = 0; n < mcuToRead; n++) + { + for (int i = 0; i < componentsLength; i++) + { + ref FrameComponent component = ref components[i]; + int h = component.HorizontalFactor; + int v = component.VerticalFactor; + for (int j = 0; j < v; j++) + { + for (int k = 0; k < h; k++) + { + this.DecodeMcuACSuccessive(acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); + } + } + } + + mcu++; + } } + } - // fileMarker = JpegDecoderCore.FindNextFileMarker(stream); - // // Some images include more Scan blocks than expected, skip past those and - // // attempt to find the next valid marker (fixes issue8182.pdf) in original code. - // if (fileMarker.Invalid) - // { - // #if DEBUG - // Debug.WriteLine("DecodeScan - Unexpected MCU data, next marker is: " + fileMarker.Marker.ToString("X")); - // #endif - // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBlockBaseline(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeBaseline(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetBlockBufferOffset(ref FrameComponent component, int row, int col) + private void DecodeMcuBaseline(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { - return 64 * (((component.BlocksPerLine + 1) * row) + col); + int mcuRow = (mcu / mcusPerLine) | 0; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeBaseline(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void DecodeMcu(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, DecodeAction decode, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeBlockDCFirst(HuffmanTables dcHuffmanTables, ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeDCFirst(ref component, offset, dcHuffmanTables, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuDCFirst(HuffmanTables dcHuffmanTables, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { int mcuRow = (mcu / mcusPerLine) | 0; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; - int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); - decode(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeDCFirst(ref component, offset, dcHuffmanTables, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void DecodeBlock(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, DecodeAction decode, int mcu, Stream stream) + private void DecodeBlockDCSuccessive(ref FrameComponent component, int mcu, Stream stream) { int blockRow = (mcu / component.BlocksPerLine) | 0; int blockCol = mcu % component.BlocksPerLine; - int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); - decode(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeDCSuccessive(ref component, offset, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuDCSuccessive(ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = (mcu / mcusPerLine) | 0; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeDCSuccessive(ref component, offset, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBlockACFirst(HuffmanTables acHuffmanTables, ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeACFirst(ref component, offset, acHuffmanTables, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuACFirst(HuffmanTables acHuffmanTables, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = (mcu / mcusPerLine) | 0; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeACFirst(ref component, offset, acHuffmanTables, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeBlockACSuccessive(HuffmanTables acHuffmanTables, ref FrameComponent component, int mcu, Stream stream) + { + int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockCol = mcu % component.BlocksPerLine; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeACSuccessive(ref component, offset, acHuffmanTables, stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecodeMcuACSuccessive(HuffmanTables acHuffmanTables, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + { + int mcuRow = (mcu / mcusPerLine) | 0; + int mcuCol = mcu % mcusPerLine; + int blockRow = (mcuRow * component.VerticalFactor) + row; + int blockCol = (mcuCol * component.HorizontalFactor) + col; + int offset = GetBlockBufferOffset(component, blockRow, blockCol); + this.DecodeACSuccessive(ref component, offset, acHuffmanTables, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -257,10 +509,10 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } this.bitsData = stream.ReadByte(); - if (this.bitsData == JpegConstants.Markers.Prefix) + if (this.bitsData == 0xFF) { int nextByte = stream.ReadByte(); - if (nextByte > 0) + if (nextByte != 0) { throw new ImageFormatException($"Unexpected marker {(this.bitsData << 8) | nextByte}"); } @@ -354,7 +606,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeDCFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + private void DecodeDCFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, Stream stream) { int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream) << this.successiveState; @@ -362,13 +614,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeDCSuccessive(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + private void DecodeDCSuccessive(ref FrameComponent component, int offset, Stream stream) { component.BlockData[offset] |= (short)(this.ReadBit(stream) << this.successiveState); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeACFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + private void DecodeACFirst(ref FrameComponent component, int offset, HuffmanTables acHuffmanTables, Stream stream) { if (this.eobrun > 0) { @@ -404,7 +656,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeACSuccessive(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + private void DecodeACSuccessive(ref FrameComponent component, int offset, HuffmanTables acHuffmanTables, Stream stream) { int k = this.specStart; int e = this.specEnd; @@ -439,7 +691,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } this.successiveACNextValue = this.ReceiveAndExtend(s, stream); - this.successiveACState = r != 0 ? 2 : 3; + this.successiveACState = r > 0 ? 2 : 3; } continue; diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/YCbCrToRgbTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/YCbCrToRgbTables.cs new file mode 100644 index 000000000..02397e1d7 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/YCbCrToRgbTables.cs @@ -0,0 +1,128 @@ +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System.Runtime.CompilerServices; + using ImageSharp.PixelFormats; + + /// + /// Provides 8-bit lookup tables for converting from YCbCr to Rgb colorspace. + /// Methods to build the tables are based on libjpeg implementation. + /// + internal struct YCbCrToRgbTables + { + /// + /// The red red-chrominance table + /// + public static int[] CrRTable = new int[256]; + + /// + /// The blue blue-chrominance table + /// + public static int[] CbBTable = new int[256]; + + /// + /// The green red-chrominance table + /// + public static int[] CrGTable = new int[256]; + + /// + /// The green blue-chrominance table + /// + public static int[] CbGTable = new int[256]; + + // Speediest right-shift on some machines and gives us enough accuracy at 4 decimal places. + private const int ScaleBits = 16; + + private const int Half = 1 << (ScaleBits - 1); + + private const int MinSample = 0; + + private const int HalfSample = 128; + + private const int MaxSample = 255; + + /// + /// Initializes the YCbCr tables + /// + public static void Create() + { + for (int i = 0, x = -128; i <= 255; i++, x++) + { + // i is the actual input pixel value, in the range 0..255 + // The Cb or Cr value we are thinking of is x = i - 128 + // Cr=>R value is nearest int to 1.402 * x + CrRTable[i] = RightShift((Fix(1.402F) * x) + Half); + + // Cb=>B value is nearest int to 1.772 * x + CbBTable[i] = RightShift((Fix(1.772F) * x) + Half); + + // Cr=>G value is scaled-up -0.714136286 + CrGTable[i] = (-Fix(0.714136286F)) * x; + + // Cb => G value is scaled - up - 0.344136286 * x + // We also add in Half so that need not do it in inner loop + CbGTable[i] = ((-Fix(0.344136286F)) * x) + Half; + } + } + + /// + /// Optimized method to pack bytes to the image from the YCbCr color space. + /// + /// The pixel format. + /// The packed pixel. + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void PackYCbCr(ref TPixel packed, byte y, byte cb, byte cr) + where TPixel : struct, IPixel + { + byte r = (byte)(y + CrRTable[cr]).Clamp(0, 255); + + // The values for the G calculation are left scaled up, since we must add them together before rounding. + byte g = (byte)(y + RightShift(CbGTable[cb] + CrGTable[cr])).Clamp(0, 255); + + byte b = (byte)(y + CbBTable[cb]).Clamp(0, 255); + + packed.PackFromRgba32(new Rgba32(r, g, b, 255)); + } + + /// + /// Optimized method to pack bytes to the image from the YccK color space. + /// + /// The pixel format. + /// The packed pixel. + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + /// The keyline component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void PackYccK(ref TPixel packed, byte y, byte cb, byte cr, byte k) + where TPixel : struct, IPixel + { + int c = (MaxSample - (y + CrRTable[cr])).Clamp(0, 255); + + // The values for the G calculation are left scaled up, since we must add them together before rounding. + int m = (MaxSample - (y + RightShift(CbGTable[cb] + CrGTable[cr]))).Clamp(0, 255); + + int cy = (MaxSample - (y + CbBTable[cb])).Clamp(0, 255); + + byte r = (byte)((c * k) / MaxSample); + byte g = (byte)((m * k) / MaxSample); + byte b = (byte)((cy * k) / MaxSample); + + packed.PackFromRgba32(new Rgba32(r, g, b, MaxSample)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Fix(float x) + { + return (int)((x * (1L << ScaleBits)) + 0.5F); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RightShift(int x) + { + return x >> ScaleBits; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index b608b4951..2c2cd57a5 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -49,11 +49,13 @@ namespace ImageSharp.Formats.Jpeg.Port private ComponentBlocks components; + private JpegPixelArea pixelArea; + private ushort resetInterval; - private int width; + private int imageWidth; - private int height; + private int imageHeight; private int numComponents; @@ -67,6 +69,14 @@ namespace ImageSharp.Formats.Jpeg.Port ///
private Adobe adobe; + /// + /// Initializes static members of the class. + /// + static JpegDecoderCore() + { + YCbCrToRgbTables.Create(); + } + /// /// Initializes a new instance of the class. /// @@ -91,11 +101,12 @@ namespace ImageSharp.Formats.Jpeg.Port public static FileMarker FindNextFileMarkerNew(Stream stream) { byte[] marker = new byte[2]; + int value = stream.Read(marker, 0, 2); if (value == 0) { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length - 2); } if (marker[0] == JpegConstants.Markers.Prefix) @@ -107,14 +118,16 @@ namespace ImageSharp.Formats.Jpeg.Port int suffix = stream.ReadByte(); if (suffix == -1) { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); + return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length - 2); } marker[1] = (byte)value; } + + return new FileMarker((ushort)((marker[0] << 8) | marker[1]), (int)(stream.Position - 2)); } - return new FileMarker((ushort)((marker[0] << 8) | marker[1]), (int)(stream.Position - 2)); + return new FileMarker((ushort)((marker[0] << 8) | marker[1]), (int)(stream.Position - 2), true); } /// @@ -182,7 +195,8 @@ namespace ImageSharp.Formats.Jpeg.Port this.InputStream = stream; this.ParseStream(); - var image = new Image(1, 1); + var image = new Image(this.imageWidth, this.imageHeight); + this.GetData(image); return image; } @@ -192,6 +206,7 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame?.Dispose(); this.components?.Dispose(); this.quantizationTables?.Dispose(); + this.pixelArea.Dispose(); // Set large fields to null. this.frame = null; @@ -205,6 +220,9 @@ namespace ImageSharp.Formats.Jpeg.Port return 64 * (((component.BlocksPerLine + 1) * row) + col); } + /// + /// Parses the input stream for file markers + /// private void ParseStream() { // Check for the Start Of Image marker. @@ -224,12 +242,11 @@ namespace ImageSharp.Formats.Jpeg.Port while (fileMarker.Marker != JpegConstants.Markers.EOI) { // Get the marker length - int remaining; + int remaining = this.ReadUint16() - 2; switch (fileMarker.Marker) { case JpegConstants.Markers.APP0: - remaining = this.ReadUint16() - 2; this.ProcessApplicationHeaderMarker(remaining); break; @@ -246,10 +263,10 @@ namespace ImageSharp.Formats.Jpeg.Port case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP12: case JpegConstants.Markers.APP13: + this.InputStream.Skip(remaining); break; case JpegConstants.Markers.APP14: - remaining = this.ReadUint16() - 2; this.ProcessApp14Marker(remaining); break; @@ -257,32 +274,28 @@ namespace ImageSharp.Formats.Jpeg.Port case JpegConstants.Markers.COM: // TODO: Read data block + this.InputStream.Skip(remaining); break; case JpegConstants.Markers.DQT: - remaining = this.ReadUint16() - 2; - this.ProcessDqtMarker(remaining); + this.ProcessDefineQuantizationTablesMarker(remaining); break; case JpegConstants.Markers.SOF0: case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF2: - remaining = this.ReadUint16() - 2; this.ProcessStartOfFrameMarker(remaining, fileMarker); break; case JpegConstants.Markers.DHT: - remaining = this.ReadUint16() - 2; this.ProcessDefineHuffmanTablesMarker(remaining); break; case JpegConstants.Markers.DRI: - remaining = this.ReadUint16() - 2; this.ProcessDefineRestartIntervalMarker(remaining); break; case JpegConstants.Markers.SOS: - this.InputStream.Skip(2); this.ProcessStartOfScanMarker(); break; @@ -295,30 +308,30 @@ namespace ImageSharp.Formats.Jpeg.Port break; - default: - - // TODO: Not convinced this is required - // Skip back as it could be incorrect encoding -- last 0xFF byte of the previous - // block was eaten by the encoder - this.InputStream.Position -= 3; - this.InputStream.Read(this.temp, 0, 2); - if (this.temp[0] == 0xFF && this.temp[1] >= 0xC0 && this.temp[1] <= 0xFE) - { - // Rewind that last bytes we read - this.InputStream.Position -= 2; - break; - } - - // throw new ImageFormatException($"Unknown Marker {fileMarker.Marker} at {fileMarker.Position}"); - break; + //default: + + // // TODO: Not convinced this is required + // // Skip back as it could be incorrect encoding -- last 0xFF byte of the previous + // // block was eaten by the encoder + // this.InputStream.Position -= 3; + // this.InputStream.Read(this.temp, 0, 2); + // if (this.temp[0] == 0xFF && this.temp[1] >= 0xC0 && this.temp[1] <= 0xFE) + // { + // // Rewind that last bytes we read + // this.InputStream.Position -= 2; + // break; + // } + + // // throw new ImageFormatException($"Unknown Marker {fileMarker.Marker} at {fileMarker.Position}"); + // break; } // Read on. TODO: Test this on damaged images. fileMarker = FindNextFileMarkerNew(this.InputStream); } - this.width = this.frame.SamplesPerLine; - this.height = this.frame.Scanlines; + this.imageWidth = this.frame.SamplesPerLine; + this.imageHeight = this.frame.Scanlines; this.components = new ComponentBlocks { Components = new Component[this.frame.ComponentCount] }; for (int i = 0; i < this.components.Components.Length; i++) @@ -339,6 +352,53 @@ namespace ImageSharp.Formats.Jpeg.Port this.numComponents = this.components.Components.Length; } + /// + /// Fills the given image with the color data + /// + /// The pixel format. + /// The image + private void GetData(Image image) + where TPixel : struct, IPixel + { + if (this.numComponents > 4) + { + throw new ImageFormatException($"Unsupported color mode. Max components 4; found {this.numComponents}"); + } + + this.pixelArea = new JpegPixelArea(image.Width, image.Height, this.numComponents); + this.pixelArea.LinearizeBlockData(this.components, image.Width, image.Height); + + if (this.numComponents == 1) + { + this.FillGrayScaleImage(image); + return; + } + + if (this.numComponents == 3) + { + if (this.adobe.Equals(default(Adobe)) || this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYCbCr) + { + this.FillYCbCrImage(image); + } + else if (this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformUnknown) + { + this.FillRgbImage(image); + } + } + + if (this.numComponents == 4) + { + if (this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYcck) + { + this.FillYcckImage(image); + } + else + { + this.FillCmykImage(image); + } + } + } + /// /// Processes the application header containing the JFIF identifier plus extra data. /// @@ -398,10 +458,10 @@ namespace ImageSharp.Formats.Jpeg.Port remaining -= 12; bool isAdobe = this.temp[0] == JpegConstants.Markers.Adobe.A && - this.temp[1] == JpegConstants.Markers.Adobe.D && - this.temp[2] == JpegConstants.Markers.Adobe.O && - this.temp[3] == JpegConstants.Markers.Adobe.B && - this.temp[4] == JpegConstants.Markers.Adobe.E; + this.temp[1] == JpegConstants.Markers.Adobe.D && + this.temp[2] == JpegConstants.Markers.Adobe.O && + this.temp[3] == JpegConstants.Markers.Adobe.B && + this.temp[4] == JpegConstants.Markers.Adobe.E; if (isAdobe) { @@ -427,7 +487,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Thrown if the tables do not match the header /// - private void ProcessDqtMarker(int remaining) + private void ProcessDefineQuantizationTablesMarker(int remaining) { while (remaining > 0) { @@ -437,7 +497,7 @@ namespace ImageSharp.Formats.Jpeg.Port if (quantizationTableSpec > 3) { - throw new ImageFormatException("Bad Tq index value"); + throw new ImageFormatException($"Bad Tq index value: {quantizationTableSpec}"); } switch (quantizationTableSpec >> 4) @@ -570,17 +630,17 @@ namespace ImageSharp.Formats.Jpeg.Port { if (remaining < 17) { - throw new ImageFormatException("DHT has wrong length"); + throw new ImageFormatException($"DHT has wrong length: {remaining}"); } - using (var huffmanData = new Buffer(16)) + using (var huffmanData = Buffer.CreateClean(16)) { for (int i = 2; i < remaining;) { byte huffmanTableSpec = (byte)this.InputStream.ReadByte(); this.InputStream.Read(huffmanData.Array, 0, 16); - using (var codeLengths = new Buffer(16)) + using (var codeLengths = Buffer.CreateClean(16)) { int codeLengthSum = 0; @@ -589,7 +649,7 @@ namespace ImageSharp.Formats.Jpeg.Port codeLengthSum += codeLengths[j] = huffmanData[j]; } - using (var huffmanValues = new Buffer(codeLengthSum)) + using (var huffmanValues = Buffer.CreateClean(codeLengthSum)) { this.InputStream.Read(huffmanValues.Array, 0, codeLengthSum); @@ -615,7 +675,7 @@ namespace ImageSharp.Formats.Jpeg.Port { if (remaining != 2) { - throw new ImageFormatException("DRI has wrong length"); + throw new ImageFormatException($"DRI has wrong length: {remaining}"); } this.resetInterval = this.ReadUint16(); @@ -660,31 +720,33 @@ namespace ImageSharp.Formats.Jpeg.Port int successiveApproximation = this.temp[2]; var scanDecoder = default(ScanDecoder); - scanDecoder.DecodeScan( - this.frame, - this.InputStream, - this.dcHuffmanTables, - this.acHuffmanTables, - this.frame.Components, - componentIndex, - selectorsCount, - this.resetInterval, - spectralStart, - spectralEnd, - successiveApproximation >> 4, - successiveApproximation & 15); + long position = scanDecoder.DecodeScan( + this.frame, + this.InputStream, + this.dcHuffmanTables, + this.acHuffmanTables, + this.frame.Components, + componentIndex, + selectorsCount, + this.resetInterval, + spectralStart, + spectralEnd, + successiveApproximation >> 4, + successiveApproximation & 15); + + this.InputStream.Position += position; Debug.WriteLine("spectralStart= " + spectralStart); Debug.WriteLine("spectralEnd= " + spectralEnd); Debug.WriteLine("successiveApproximation= " + successiveApproximation); Debug.WriteLine("Components after"); - for (int i = 0; i < 3; i++) - { - for (int j = 0; j < 10; j++) - { - Debug.WriteLine("component [" + i + "] : value [" + j + "] =" + this.frame.Components[i].BlockData[j] + "]"); - } - } + //for (int i = 0; i < 3; i++) + //{ + // for (int j = 0; j < 10; j++) + // { + // Debug.WriteLine("component [" + i + "] : value [" + j + "] =" + this.frame.Components[i].BlockData[j] + "]"); + // } + //} } /// @@ -694,7 +756,6 @@ namespace ImageSharp.Formats.Jpeg.Port /// The frame component private void BuildComponentData(ref Component component, ref FrameComponent frameComponent) { - // TODO: Write this int blocksPerLine = component.BlocksPerLine; int blocksPerColumn = component.BlocksPerColumn; using (var computationBuffer = Buffer.CreateClean(64)) @@ -776,21 +837,21 @@ namespace ImageSharp.Formats.Jpeg.Port /// private void PrepareComponents() { - int mcusPerLine = (int)Math.Ceiling(this.frame.SamplesPerLine / 8D / this.frame.MaxHorizontalFactor); - int mcusPerColumn = (int)Math.Ceiling(this.frame.Scanlines / 8D / this.frame.MaxVerticalFactor); + int mcusPerLine = (int)MathF.Ceiling(this.frame.SamplesPerLine / 8F / this.frame.MaxHorizontalFactor); + int mcusPerColumn = (int)MathF.Ceiling(this.frame.Scanlines / 8F / this.frame.MaxVerticalFactor); for (int i = 0; i < this.frame.ComponentCount; i++) { ref var component = ref this.frame.Components[i]; - int blocksPerLine = (int)Math.Ceiling(Math.Ceiling(this.frame.SamplesPerLine / 8D) * component.HorizontalFactor / this.frame.MaxHorizontalFactor); - int blocksPerColumn = (int)Math.Ceiling(Math.Ceiling(this.frame.Scanlines / 8D) * component.VerticalFactor / this.frame.MaxVerticalFactor); + int blocksPerLine = (int)MathF.Ceiling(MathF.Ceiling(this.frame.SamplesPerLine / 8F) * component.HorizontalFactor / this.frame.MaxHorizontalFactor); + int blocksPerColumn = (int)MathF.Ceiling(MathF.Ceiling(this.frame.Scanlines / 8F) * component.VerticalFactor / this.frame.MaxVerticalFactor); int blocksPerLineForMcu = mcusPerLine * component.HorizontalFactor; int blocksPerColumnForMcu = mcusPerColumn * component.VerticalFactor; int blocksBufferSize = 64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1); - // Pooled. Disposed via frame siposal - component.BlockData = new Buffer(blocksBufferSize); + // Pooled. Disposed via frame disposal + component.BlockData = Buffer.CreateClean(blocksBufferSize); component.BlocksPerLine = blocksPerLine; component.BlocksPerColumn = blocksPerColumn; } @@ -799,6 +860,104 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame.McusPerColumn = mcusPerColumn; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillGrayScaleImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + + for (int x = 0; x < image.Width; x++) + { + ref byte luminance = ref areaRowSpan[x]; + ref TPixel pixel = ref imageRowSpan[x]; + var rgba = new Rgba32(luminance, luminance, luminance); + pixel.PackFromRgba32(rgba); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillYCbCrImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + for (int x = 0, o = 0; x < image.Width; x++, o += 3) + { + ref byte yy = ref areaRowSpan[o]; + ref byte cb = ref areaRowSpan[o + 1]; + ref byte cr = ref areaRowSpan[o + 2]; + ref TPixel pixel = ref imageRowSpan[x]; + YCbCrToRgbTables.PackYCbCr(ref pixel, yy, cb, cr); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillYcckImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + for (int x = 0, o = 0; x < image.Width; x++, o += 4) + { + ref byte yy = ref areaRowSpan[o]; + ref byte cb = ref areaRowSpan[o + 1]; + ref byte cr = ref areaRowSpan[o + 2]; + ref byte k = ref areaRowSpan[o + 3]; + + ref TPixel pixel = ref imageRowSpan[x]; + YCbCrToRgbTables.PackYccK(ref pixel, yy, cb, cr, k); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillCmykImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + for (int x = 0, o = 0; x < image.Width; x++, o += 4) + { + ref byte c = ref areaRowSpan[o]; + ref byte m = ref areaRowSpan[o + 1]; + ref byte cy = ref areaRowSpan[o + 2]; + ref byte k = ref areaRowSpan[o + 3]; + + byte r = (byte)((c * k) / 255); + byte g = (byte)((m * k) / 255); + byte b = (byte)((cy * k) / 255); + + ref TPixel pixel = ref imageRowSpan[x]; + var rgba = new Rgba32(r, g, b); + pixel.PackFromRgba32(rgba); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillRgbImage(Image image) + where TPixel : struct, IPixel + { + for (int y = 0; y < image.Height; y++) + { + Span imageRowSpan = image.GetRowSpan(y); + Span areaRowSpan = this.pixelArea.GetRowSpan(y); + + PixelOperations.Instance.PackFromRgb24Bytes(areaRowSpan, imageRowSpan, image.Width); + } + } + /// /// Reads a from the stream advancing it by two bytes /// diff --git a/tests/ImageSharp.Tests/TestFile.cs b/tests/ImageSharp.Tests/TestFile.cs index f1b78383c..d274d61a2 100644 --- a/tests/ImageSharp.Tests/TestFile.cs +++ b/tests/ImageSharp.Tests/TestFile.cs @@ -12,8 +12,6 @@ namespace ImageSharp.Tests using System.Linq; using System.Reflection; - using ImageSharp.PixelFormats; - /// /// A test image file. /// @@ -32,7 +30,7 @@ namespace ImageSharp.Tests /// /// The image. /// - private readonly Image image; + private Image image; /// /// The file. @@ -46,9 +44,7 @@ namespace ImageSharp.Tests private TestFile(string file) { this.file = file; - this.Bytes = File.ReadAllBytes(file); - this.image = Image.Load(this.Bytes); } /// @@ -129,7 +125,7 @@ namespace ImageSharp.Tests /// public Image CreateImage() { - return new Image(this.image); + return new Image(this.GetImage()); } /// @@ -144,6 +140,11 @@ namespace ImageSharp.Tests return Image.Load(this.Bytes, options); } + private Image GetImage() + { + return this.image ?? (this.image = Image.Load(this.Bytes)); + } + /// /// Gets the correct path to the formats directory. /// @@ -152,7 +153,7 @@ namespace ImageSharp.Tests /// private static string GetFormatsDirectory() { - List directories = new List< string > { + var directories = new List { "TestImages/Formats/", // Here for code coverage tests. "tests/ImageSharp.Tests/TestImages/Formats/", // from travis/build script "../../../../../ImageSharp.Tests/TestImages/Formats/", // from Sandbox46 @@ -167,9 +168,9 @@ namespace ImageSharp.Tests AddFormatsDirectoryFromTestAssebmlyPath(directories); - string directory = directories.FirstOrDefault(x => Directory.Exists(x)); + string directory = directories.FirstOrDefault(Directory.Exists); - if(directory != null) + if (directory != null) { return directory; } diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/baseline/ycck - Copy.jpg b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/baseline/ycck - Copy.jpg new file mode 100644 index 0000000000000000000000000000000000000000..30e88773ededd890ad97f0a5cfac372e1af07aa5 GIT binary patch literal 611572 zcmeFZcU)6T*Dt=2E~tP73ko83dIv#JkfKrq0YL>Rp-2@HNJ1LvAt8i>5^CtZ_l_ch z3U(=C!4CFXuIpaI@B3^jE?QGj5!z*7AS3jmP4>{mU@fNa6P(tJkn5)lymsjJA} z+as{!ul5M2{=GfR1b)d7&|UUhw$zR#aex*01TC}lTo_q+2S_fq#KX)6ShnyUSh<)3 zd~kfeh5G;zkQ5UW6Bm;d7nhWlSS}&Gc9o>$sE*i;9Q|3rcE%h0=mT zTV*ta4;=Gct#~d*Q)Ep-PR+q>N^2c1cxh>e<8PUXZf84weDcXVLT6oWEn$Z_=eW}{u~kk56*i+5npCM|X*=9B7^dhR~|QqXW|@Wqs~ZzMUnu(5aO<+Prqi(eEa zrKqWIc>Jq0uxuHqtS3q=!PWNq4i9A`KLr%I-$kX2{{P!B{{7Z#}Q9x2~u}*2g1b8yr=X&Ff z>-6}z+Z*MYo*E+i>WH;+ZnFM7vvp93?NGB@iGAp>HJD;sD<$T$t+#^&>wgkXPdxN9 z$(>qW_c0h2>HOIJycL8YBzq@9bTVnAL5Bz^A6!vU&1{?}#_v;@`u8w;xYV&YFEz z5l%_xjc+s7dJfQMp5MD9cG$4z#H|;9W1rZ-d;0x#gART)pVs1s(^yKiSsg&F;ad+d6a%f>w z8t$;^d9-cR>|yx@!0b*9GIn70(yjC%r1z(E!5v+r<;CNisl*N&OG^hJKgZ}ser1>i=c>Xl>T z`_Ijd-QQ`FIUjBUn!S~y=5XVY2~t-z(pe>|z|L@DTE_F@*V79C8o3POe2ftz0}z^3 z{5dk)SDOc!-S&R&lwjYO_wF&F-|oH1j8$1i@71PtW6l+Rmen7he6L&pI$J{Y^rS~B z2Vik!TWWOgd9IM0x9NH_Yxglwi~FL0>7Jgbbk_L8*V7>Lq?+E!c&2LH{z1;xqU+_p>s;sB%Kd&E?(TZiHPlAjSKQh7 zap-ohi{N~d`vNd)JvLz*sL$xoZ`WMiH?O=+ZRT1#f~dSXwC?Q%?=ioUh_QRuH9l?` zs?zA2{V-J-pL4ey{{H@nev`>}(aC*BzzP*w8el`VRI?(?z1i(^B=xu8l8J~7u7MC!2Ut$uMfl!>_WAE zr1kV7%!pjb@noY_Jk?(!xCru_F2DYD)AF`W?(wYT@-GEidld#5W@8xIh zBRbXQ$K$^rSO6?S&RQ!qX6#(Jvos-+G%A3TxmC*_h4Ds5e|38D&@!R!3|HNgfP2l zfo`XsZXCj&dX^1{M`X?=^}#R|p$=_<66w_8RTc>|J<%s&6+gb8Zu{afk=!+O>K#|P z*Qep0EoA|?RQKT1T+?hIcQ}=Bd0WEE{1+eBKPWUlxk~g1yH^tZ@clQVPFLA;3xLDy z$MaQ&Y4z%vX*9q>t@Gd`?pbP>CP&Vfc&a` z@9T$uyfC>o)EheYwfKvB1q=>-`AD~R^irzwxfmc2K6k@^3R_$^8~^R{0OWnBMM>hV zFDKFO)EZ{zbZ^UzzMe076?EKy*A*ev-+xfOWFq`WyZX~%xvmLR*R>{tQ@0XYmM5-~ zJb$0wep>z$XDV7tO>I-@8HYDxuMFEu-vF)aqa6LKX8OA_Yr8_o!M1!}0CZnH zdC`J;iqJ23CE}{6C;t$Ri5m)iDW5i927yH!J{ntZrNDTS!CW=mYuF zaVx$1g!7M_YQN!bt(0kPeC2&>Rq5#?Q~Sc+rpQHwl+DJoUoHS%w?Noltjr5y-Uo9e zNqhltGwGOLz5tLSG@nh2H($MO^7=BernfPuS};T)+;xUoS3XPcvsQpS>Fuy-1z$ipt3alNBP$2THoFk&r=n6xu1Si-EcL9 zJT*$tco5UVWG**oF+#~1C_?DFTBw-v*)D`pmI1?8E}TX_lK5<>%ku6Q?5Roh*7Qxh zlY*MU>1NrY2FwSMu1z!7ctdj=cMjfvkF|iz--8wgdC=;Wfo?$OT`9p{r9JJR6ogayFSjSYXX((PO#)6CNGS@qc` z{c;O{)Fq=6y<2;XHRh{I!|%*i*|Rrk`eRoC-}c#ErH$Y7VOc!QN;txL)AhO2?o!v3 z9dh%L(guoFqIZVB z-#&dkVxT=mUTZho`t(NsjT7OY=5H}y_uZL2y7MbVejF`KDOR`0emx8)l$~l-G!@ER zcH>&_hLyTQf185&`eOD5s)(ql8^XJ2z4J#W6usxln`Kfb>@H@aV)Lo;4|Uor+8)iQ2%QI_!sAZ+7FM?mgjJtAaaVuFWD;9nr`p;-*4ZtQD$^k@i_f%^UI5&7LayN4$*Q`yV|bSK4`c-SY_pL!C~)i3X^?exK}yhPIi$ z&C}Qu^P$t5-Z`nO8;eR*Qrz~_4OYy|!+28?R#N69mvqLn^&`bwx`yU%zea6JWs(f32%-SLH{)%fYU5B?A^~lfeO@E^{Wmxcm`oPWmyW~l_ zXz2oQRTk?vpHmv*tg%6SS;x*vI_J}cxsCGgEaxQSj)UKV5Pb_nOT9VrY5}-&c7Axy zZ0L6P#hN6aVF-EsJ+^>R!?njKXWly_wXvuf<^piJti906Lg?_p{gp-CD*FTFUZ{J^ zpp7?K%K)70C)epS2mN<0OG!L>wV>_Ix5+`E-0S>5wNy6|28yK@0f*Z@z= z_pX7cuiL*L5&imb-n*+sTrONhQ-RPNWXUC3C5D?z*ED`ppo#BMdCj#kv3Z2)!-nsC z=gyl?7<*{|@q00_ePXKTom;|_osO$|5Qx5EwiN5pb6}DlaAwM5=HQ373&35CvzS59 z_^&qe<@@+OJbf?uq-(OFLV}!5#p55+FP?1GwAyByllLjhd!?9$B)+KE8-$D&Xy}7R&st#dcxPfY6W# zuw0hUX%+?z`PB%pkzV1yGY)DCBVV*pc$wQ-(dzg_$Vm&gu=W`Lm%_Aid9}xqLTK0_7T5E z1wYMa2A_Pjfuj)LB{Ba|?Qzf__ixoYdIv4h?ZdrIUHK9gFDdWv&@dN&2-vyc2n`7F z`(?hB`~nA1r-PwTXlQUyXo%lWh~%#lz6yV%ulPlG2=Md&dx6BS0#LW#{QS8h_-;vu zpPsp8zn+WpXvu50@@J*s!vqH`jkgtxG`@#&%OdB7f93@z7xUtackm{#hyuNr{A>Bo zNdgYoMGdAFsaAlRuP;~<1l~bkBX|k?q~xTg{ibLx(%r!OFCM;2Qvl$P{GXIXncu)X zz@J$Q|9S~d@QeSoXyN}&*`h7@qQSQPv=raF`&UkVF(hw_wn+Io zhnJaxK=A!j(iVPd(Yw0T2OIfWLH@e3)N+1y(Sf{Jr7REt1OuLcAGpedEv`D=U@8K9 zg@QR1AP9W=fo~N+2haeP?7Ar7AGe|Is+bz;S87n+t&`?WA2qfGS3h@C6 zz;Q&_uaA%AOU!%9x41{ujCg*v0<;$-X!@ckwF%Kf}S{Z1Qn2y)+NkET;KGcxevvIRL4H;Eg|f zvX|Zhj{yN?FbPIN)`O9aKar0=k&i!-k3W%*Kar0=k&i!-k3W%*Kar0=k&i!-k3W%* zKar0=k&j;yl0T7;|H~sEi(ZGVpwj>V{dk}^f$vUO1sni;K)0P2=*&|9!wLRi8VbH6 z7E@s$$ADW`P(h^1Dt& zU0+3A=eUNpA$Uw#LvOE!x}m!I601$9Pk=AtpRAf%$A2AX{s${KZhXMd&Hs_v$J@|1 zG(6Z7Y;{1eryoS^Y#78(QQ@~S4HrF&V9lU<+JC28o0);}H{XCDu)s;xQB}bO;sp*$ zz6V%gm!pfF{ceRw%|*R!Yz(bKBA}ij-VkeZV@1$ZssRB$hMqbG-kush`YO8KS~@CP zdfM75`Ve0q6&+1=T^$W?bx$2n-(Pt1P;VH2K=AQPbHgXp8vn)+Ivz7TC4FBM$_ zbx#!y4T!FazORp`3PcBD0MYUE@zOQ${3UJ4aqSuOcd2}>d_al1T6$hydYXDF20B_^ zD&Fea`YK*J+FmM}p6Z%zk=a=^dX<9}51Pka;@Ap74Am!;YxLVcl8p5c%KexU9CXJb2JdY=|ti z^`9pBmud0;*d#w~?(Odx;s*g2b2UYNi`4kb-cpaN{pVm*B$= zfEX_9fce2g%fPcFVd2nl{z7fR3kkMA!j)`=j9Q72xyopMSf+ z-SYI%VhA3%qw+81e`EdETy!v~i90N+q=Pzi98%->({je>^p(-~lT%>m7W>R`GW z0CGjZG?c%5XJcFTt=!XXY zz)JX8FDM?^_}^|b_@)0JUQqD32Vb07Xc%Jg00g+FR##V8+Iu@9BA}4q2xSH95N}lg zIN|E1uvB#eco3@{Jd?&&5B7!fj}QSug5a4izMtan*T3XJ5_o85nUc_E;Ze~5v3tvP zB}kHeD;8F1NV!YXWtvt$T`RFpQ`TiYVgq+$!=~GEAGU0lcU7QltyLV|wy=HM4)dM9 zyU4qX_Vn$YP!UtzrDmlbph43t)aurLr1M>Gy}pjYF~i_}G@}CJ?){HUz8#Q0xZCWY z`Kd!Ni+IZttBckT4}U!>W20;Yojn-n5ab^07jiBX8AdqA2v3a2fabwUBWt3X;2nsI z$ji~!P}kAdFay|r+?AL<{3Sv+v5VA3ZlcstD`_RM`SdIXH!gumj*n)ACHSyUaE>OL zCTS(_NZG)Ze_EUpn7fnb%E<|ZVcY+yj66Ybq8_RbI^K7V|e`tF!J)=wb9!9 z+z04~UXLutlpjky{_-l~6_cuR^=1$E^%-1it@qc_QFd}YO1I8D%sY~Iw9uy1w#u&FzV+C7hd#&a z#|NE;o$tBaKk>-*vD>qgh)i zgWrTqguV!Sa_&+1Xv7fo8th_ZOH>uS0FjPlMUzlSbSTCLdje+@a{#YH*iDopNs;Es z?Y#oZ*&fm8Hwu zp1meVC}%47W!^~s)q;+~nxewu^pb>9N*T61sv@+~uj)*-Tg~xWySk(G)(sYohnmcr z54BjdTD2W+x9xD~bm=;M-uuGY?(mDzJ;Y0~y{x|E%b8aSu2%Fn4s>0+eEs%~2RC2d z`gHsIU6DbVp)JFEM)dBPj~=`4{vhxn;t_c)@o~nJ+^5CQDxTNAXnff^-Z63h)y3C$ z-@JG`^jOmm4nL3fRFi$9rgigP;IoeQoDnVvkaZ0{VOJV?H8pWbCrouk?3}r*R|9Yw;IZEeTcZ0!~IEJBgNzOM!F4QqQJ&r@LpkW;$g#@a(hg za_n;L^BnS>3S0|4ihPShO5mmVGG=*3MOkHA)j;)F&19W;y+VV2RsDsaM}F|{%T?W)q!`{rEX~6Jbnv$JMm8K-5Y~%hDAoU z+|wOBdf)Q_^daRDcdY1f-ILCzeb25vzxCqw%bVi^6Fsk5Uzfj0e@l56`u@a+Qy))$ za{hdD()i1sDVeG1>7lQ6-{NMxX4PiDe6RW8J10F?H*W*}%Iw_Y{J$p{CoC>C+qRA;OIRPX0lg8r$yd&4^MNhO^0Eqm z!t1Sf6g##RDzUaBclhkI-DR+Q`yQ!1b9-Ma53BU3)~IEvGc*vIep*i2<~o|Xih9!e z0{Wi}o*LfS*JIRRT)3ZW5^IV+5PHzd%*ovPkdcMDYYFRx!;?oQjy|xtX?w}8 z#lGTLjzbbS4v_A9xI$>F_M~O82Juz#+cA$Ndia zs|Rd3D{=N);ESN!!JQ$cp~+$Rb3x(G5vI`HFd5kQ$mdbl;EjkZBsn?+<&55s*^XU- z{ert6(~U1C#1X?uF64a_1*$OhHSI=h9i7WS$9XX=<5gHPtZxaA*gc%m#DpYx@|hG% zu3GB)G=a3&>31`_GE1|Pd4%k9IUc!oc_#Vl1&W1hi^PiNiYH56mp(1KUp`oIv$DUc zueztEyY@oeh5GJ>OO2PCt~K9jx!?M%?Opq~PN6R8^9mQ#yG<`T^!Q#v_Qv-WUhcdy zboJAK|<8 zy3$vR+MB+n@(}7|N?u|Fsw}Y(xe_gz@X|dA9Tjh9+K66=Q`yMC^w6(-6_2*8ggqLK zHZMW;Zi+sg-&1}&Iyu{yBNhER!wo))I>~K2O-0>EvN35vJFw{+jL^Nzk5eRMY3u2S zu1H?JRZkJJu99AcLVheUPIwx9G~YI=Hu^lT^%NatoF2db4yv6Sxn34^pJn|;AF0+G z{$K@iQ`f$WUdWxz5v4XrpITkk81hQ_xyakm=0#E`E2Gcns_&OX?c=SIl|zZfXHRZM z+`4^v^b?}~s%N(~qPaV+#02rN%`<)+=~{n1QUv+1viGD&w0jA~*gN`W{+)H!(GKy_ zpZ6dbW5j!N2;adw7n~4h2HqDtBNBU_#;-=qcd%hckmM#&w}5ELng>Rs(a4hRYqOA- znVIk2(N<&j4q>POx~B6QMHuZ;*g#fCYcV&Hh-fS5P2vwU!!3}Qj-D`P5_uTp-3%#; zDER6-IgIn|jw2~50oE=>%!uERzlJasx0+5P!0685P6S!npvwY&go@gyjDJH(TuUWJ zQ;H`XND*1zZq*VA=`pQXLM?Y+ZVq8(l3}bqKAW9$Za3bZW#;@9zlkYlsEt=;B(KpY z&|O+J7wlm-^V;llL?cE$x9x0UKRu6EEqV(klB)} z*W%L{<1f64k_}$hhX|Xi1)Cc14yCWNg=1O@RVjlpwmH2ab1~wX?N0KzXK7mcG~6Ut zUq(KropIo~8i9U(^jbMSqZQxCj=5fs%=!|eTRBFaz}+dS4$;7I^4}dlg@a^|=smCVpd=F%+$d_FRUTi;$&KpL4(+_`fH9x*w$(* z-CNl90x79I*e|i~9~!YeQQW>j77}Gv%gp7JgUY4Mx&xN zb(yrC7y)Tb`VjWfeTxJYa(}OI{6P}2dQTjI7@As3|3P5k39%LU&u1C5I6U>3J2ek) zs)M9jFkTDahlxdiWoXMDJ5k~>}vKLPL?{s>hyP{ykxf9ousU0 zENEqtyBV8TuA%Ot5r#YF zZX-$Z;8kg4k|Z)sB)5=Ks2X?j82!C{opp4<^VTKnXzs0R*yEHh_136^_gFT zuK*S#dggFAa}>o<^JeOxG0QP5dF=Uq!?dTwxh7xkHo|zGKvF9HN8BvO27d&;H^D4s z#1j<{i%GM_#C?s4QANbL#5@zf!AvKJUAdDw#;jyeUPUQk<`I92SL6~xBmAEpsmhqL3L z0!+uuZKsYz>Y@FyrwR}3xkoF`m=QfplOVpo_%u0W08#D7xq4|NV^@N6 z7l~pUzpbV2+hhlw5=-{vxZyLB z*Jdq`xrslMHW#x$_&(PV_vVCmQaA4U0X(N2cYmigMoMRNkhfslqv4 z9tX);$Z$v9;+2+h-D@==TKY;@SAx=Hy%_o`2!; ztXCOY?D_Q4R1>T-w=kI=D3mmksOof&gJPSQI3!?LZ?}iAx)PiP?=VyG$xV4VmrIrv z=wvw+tYxdFOXmb)@LcJv4QCCKoYNJZ6xnaM<@+zP-lmvr7hvs7mJqCpyNpLS_UE|N zcH{?VKB?T25R{f#(u=;Dl3j2sAUW|(uEX(UHjYQwZ^sJCI<8b6pUoW*Oo-cv4`^JK zJ>TA#*PDrN@?>32Gp;>_R!%Xg7zhwZWR^TWF3VOaT4DTwC6j+q2@@ItRXpEIAdW5Krip`fry!5fm<|6IwW-=p?FAe3>rlaE&cBgSs=Kvt- z4)$vK^{QQXLb`wH^O#f`rf?MZIP!bmYMi%sF)tmr!_F}Si`!-ZO%ui)mCs2{jsbsc zn#jZfWn-1bbk(%jl6hJjwWHu2bz`JPt~&X<*Ym7)vVtuyy^zGzA52vy9g$b%mXR$0 zfD;CyWGc>b##2j6^b*o20|n~wM3{Y!e_V%`Oy&>9F`J3B3A(=i0q)t@+$~pAhUs;n z9}12GN)MD@&8X*&7IRa($&>ksDY8&l_Kn2zo;I0B*wr=%(~J{#>2;+vvjVrkQr56M z0f_`QPPBwou3Rv{RV(_Dt3kHPo5^|_aWz{f{ll5kjM~&wN2^lfQv&sDQkEyb*s?3x zBk=^ViPeLXDSlekQYDy@TZArcCUxh97ha4o<(21|o!OnCn{9XWK&o8UKHceLTqb(+ zWYXnCQ(zZs18!6C!7|TgXv&77O?A;Ev%D3R->UwB&D|fORIZCwMN~j+DLvM=v10Vdgr>O5J}QL^dz^J zIG{eudrRz*jmefHgFl#x?*~!-O_OXX#<#jv@#CnCl@4*=&z6>4XLz4L7nIRe4!Pz` z$GWH|@m9ww%bw!N#e$yJ`29E_W3b60Er8)w>zPs*9bHkFqAgwy6v za!gn{>ie@CSbJpmXX!FQKP+Dzu4m8{tM69bN4k^?mX-OxDnb-%IFIEWDX2F4 z!Ar`|Qp?C7XKR2cSw1T65)Mea*Ff%g6Wd=6Z6+XBmgUqn`j;1Ns(R*JoBN?6+iZ-d zUs|M=kfD~nTUI{3F+LYWf%7Y2@{%`VJzl=+HoQN4T`LOS;+fYF0Do?+?f*Jue4Wd@@3dg!>9F^!#**l*{$TUgk`o`>s$F;DGRRApg$j4R4K zFfVqWF80N0t|FCO#F+yCV;%>vu3cnPi(@Q1#V9+^xi@E#9-O&f2P00} z+jAFo)#W*+lca+3%;}o|fWCkOn0LEAwp_thwyM;phrX*%uU7G(R2?gywT&(fD?4R` z1O2>HyB_3TPS2LA&TXb!fv7h)fLMQ%2cLuuUv-5Od@FlG;U`_BJBQ$Ci@FvLd{jrY zK@4$1VY)U7Q7QVS_Fgm?524L~D6bnlTnH@v3I^-v8+MU_sc^pDF@!OMC=>SA(P&V-h#{+Z_!y< znU-F$^;(%`^8MurrSg>RAPNSe9$ZbUD1;sAIbIU!d$@B~p@4IDvvD5Nf>_&~o1(jw0|Lpv^DX-MmqPuo`@t6*^ z!Z}}45mItL<$#1`aW#eCE-DWAOpdld#EyQA&Oj7&&qYroZk3`@Cy;UpVW^A9*r+!2 zzUXzQrqLIpOHGC_aMY#(=bt3IAWu!tY-86rgq6Bo`QaRi}~wa9tAEo*Z$Ddut{ z0wo*cbFvjx8?$_W3fd2McLNOLfxG|yEPMwGI_wWGXCCVsM(8n)6|oTmu{>rwGJv)n zrWY+u8FWjCZY3ufN1_5rG}&p?2h#et+{kYkB7?jr68BE00(@iAtwI5KJ7-0l10s}F z7%_$1$qaObA@4J6jDn&`vG{dq(NyZ#>yMGbMR|9DNKRf*du)^nFEpPNHJsi;{{T(?}_#tK42K!*hruf0NK1dhFnpb$(kvfOlsj$IH#kSp%K_$ELbfPxoV`4j^ zMza;qap6T7t6hQ+fm~n1WaP4>$~Ddizqs=g_RyBD&9{PK#?4<^=3&=s7jjoeA}f4o zEm3C0%3*czb$PbV3Gk7uHwHrp-;6!0!{LQ-JH}n0NBbpiBA{0vXTQV(hF-Gyr1gPNf?i3c z<8L7(DQR0^kn6LE@V=l$m zWRArka0BFRF=jYua9zx9oayl`G3vN2dhcThIAs|)K>=6zh!}G?uI!2}?g{---7y>` zRxQI1cZ_uOC5 zDZIit|dL~6e+a05smG?22H&OjLhylIq&mg+I4Y$)S+f<@lzUWi0#!k6y>%h(SR}in zf&L~+q(m^bBnrd9(3o&>v<5W=Uh6ANxr9L4WKnh@tu^&2+oLrkhiFPD#7!gmCtP1U zDmE3jzi5=^jlItHr#fLVD0xZ<*2U*Lxe|NqXd-zF_LPPWc_Wr9@s>izxm|x6TSDK| zww2~V%P1_T?xpG_s8Q4@E0CYbC&;V4pOQ*Q7DwVqJBZ2Z=A^AebBQfvB|`T#3@tth z)>1>Y=ExT8ploN2#$O|=GH)PiNI`KDUVX#?dgS2@;<4DZY9T}mn(OjF;#+dlzz%9P zZ?xHk5|mMvcb#mTn#GJEolfqC6N%i!EuP&(v4mUJ=Lwwno2pd=4D*xtZNd|B{?%zJ zQ0m{LM(!v)n_EiC%B9Bn6RWdEqws_e>7i%t5KeI2t-j-5Cp}XY#otU?A%2VikdI!O zp@cVVXw)NXRNLm%kXDtYFknQ5qK%P71mE1RXJqlWd6rgA_!Aj0m1w+vnzh(Vyaie4 ziZVs8YoQ^Sbg9)ZdyL4de?-qAlvJ@{kMJK#yFL8yzJ>XgeKGIy!2it^)0!0~79Ued zx^g*zT-k42e~kp~8Oc^7T6ZF2UlWcr`@?kb%sRk*5HC{MV0k!(Q#z!a662n=L+o&j z4k^7`G`Rytta43^jg-h(!6rneP(HFMqjbZ+#fwHwo)(Eqg!dn6VT2kqnU% z#sVtie0vfat6b^GvB&hMuSsykU?>9dRE%S|c-%XT-6=T+6%%ZJogRuw-LsQ!hM5&n zrU&6UWkd8Af`}gp+7t3yjyq-jloNY@(Xkr`Q-m>u&E@8$ye4 zXY6vqj?Pfd0d`T@^MqZjRBlVW9y5n{IWCOxEc6Zi3*FaEJ+?Sj&TIoMizdCRlm?-W z3wuz92{|2>Y`+Xr=?#``YHdm?(=$boki;lWB!%3g?_wWtHKn~_IUIDPK4H4*okHdU#Ln_2iw{B#iF0~dFF@w(wCZ1%#TODv`VjJr}2R^0gRogljQNCBmn5vN9 z7WeG1BFCgY71~Eq!#}LOn2m&9%(2R(!ffM>()wUeQT|-xNQQq+(yJ&(#|h3TT+=v* zeF!1B&5v!3xGQj&<%(*pspg$QzsyEw1fsc2RBAceJGwMQ34P4(ZQ=;p*HMXm8=Yx% zKfx6JM6o473v*mRnK^)cSlyOoL0QQ|q@N;viHql^6K9Z($qGa*KVi;8LWYBV!V`jq z(Pow-{=MQBmJVT`fOK3auBECy(~+f-<)0SJJi`b{sb@4Inv&G%TE45cjVvwZ zd&8^DqPPcJWtqWDHQ*)P7?)8onLdywmQl`q&$|^{lB}Q68TFXcp8CSaI{}^IVz&?< zn`CULA2**Uqfipp#C{A6#d_lsDm2q=%Cpnoazl$R&?b_a^5>(ra}0AZ-mR>KEML2j z_~q%J47%garY}=)id&cT0Ju!6#8JvwX>S_OrrUE@)I6hEB^|4PMUvRpOPswGSe$}> z+n3Cuyu${raX`*V1xW@gsSN0#ZNlNo*Q8~f&rG|M(%BYD?Mwn1onc?u@S0pN9?PKO znk|xfwCuG04@PAnM81=;KPeKZrxxIl`QoJsPy-IH@GZ0*Uz3M|*_{&xKhJ--M`Rw3 zymYudtvD)I$A#Mo58Py$8VG+2?xeD?(s}A7$*5->iGp!dI$kL^3>6U;$b@E24KKQLN3I}9AE4-Whl9iIDlbD7r;dQgIAoonYtm(UeBF zZ`{!&vSoI%0q2EgTjD+rZ38jUne`e(iQ)iWWyhv z5Uy>;Ny|M+wP_qpFHUBf)CPM_Zo*~&prSz3x&pIWZRX3I{7NbGm#l52YlE!PMhlUy zQz?)05-mEDj^?h^l;)`Npc{5@q!PdbL{tgTQ&E~f)Aopo&N<$+6-~^%SxXNbP9szz zU0EsQGJ-`=;)jxh8rRt)IpyoWvKv@W0Dw}B1Cl;9h(+{}(bdgRy+~GhF0>o+q4*Lk z=(u0O-bjrDxw&Fdg32Fx-=fN8LUQGz%E5MFK$KIxB1(WPUv(+EK2p0ZE1Csi7S%>4 zJATiXj=p?AKl>GGqjG%K6;y!C@$5`g6{sQTb4VPl+dxz%F)A+-v|-hy96ZG5M`20K zh+}A;Xbjf0gZDV*q_SFOeT<)s3C{!%9-pFa1yQ!O@(f4f+lm{p-O%qP`Lr`Wt_3wz z6NjPP^%Q?o7-MVKq^=ih9jA+_v26%>!3~HG0y$abBguhxQTPGbpSa`GY zlh>Zyt}>MU1D<$E#Qv=GyM@>Gw5R%HypRz|eaQe%0C#_Qv6=&}? znnCxT+*xxKI&e6)A`3>*=ajCAv`}&=-W%Dp{6wi}6zHoYOM<8^UBc0-h?Z6{i-;{FCJM60!xNq+=jzH;CHbzMOpqUeL6T-g58j8RRQ-OSiS=aFg>SGIA0r_drw{h_Y$*Dou(SY;Y^u;@?{x zm^baZs;n*tVwG0p&U>M^Ca;jUd|PSu5AJ4(^V#*3&mbxqM6GC*uG=4VzW!U4fqzrg zWEtVaRN1!TVk>H4N0EWv>fE{fRol9=&u~>FK4ovFt_D$_I3PNtUn6`jIQ3Fvgr3KV z^Voj6|lt7gKRZ}E2xU8oeDed9j z6^OWJAJ%G(7&T38pdeSMIn)#*oi|xlYal&@Br4TV79dIyM1^0GiMbb?axn`x;$hio zj9X=I+aiI@HbvJPVuRJ}t1Yono6wbPY?)9>xit<3qQJQv9o9ENTN8}#j-X_D$aYAR z{p`0l-6fGs-RlHM%hV26EhCO?s;JmZA_~itrV~Oylst$!+546S{v_~17!&6{-!80iATUdisN&xknB z3d*d3-eMjlOT%6=B72Xr5zds?{wE zFH0TI*c)*vd5~BMeZlz_go+eR2yomMCGo#_I_t2imamUvfdwXrqM#xmC3M`qq-lIjaj1)O!dQV(98&q#@<&R zo!O6Ni?-ZOg1#ISA3F%2xFfe$gmCN%FpMDmTh}s?sQd;a5Q!FAwx;A@)YX_ZGOUNe zW5#dHe&JuYU{J_m*Q1YNjD3zh4RHS8MEz+*b)P$fgFM;M31~$9Y92`L!Az+q1%1MB zYIe;4V%&t_o2Q{J=Z+nXfq5T))cq2^fW6YOSWGd>X3UKUeQ$g~4HC@3I(aSE_^( z6ey?7rXU?Q&^J?9muyHEdCB|j$m6-u?HkB}oESBOj9|kkzsY-8`zq=wjx5ilX%s9| zzgk6k&GfL}O1;QnU4Rm~O6Ps$#J%$Qt#!n&Qh${_DM}0=x0C9GcgpeP=>lZp6f%~- zB%q6Ym*+YiPm!>Hp8rTdnx5}@OK3MvYw;z#)@@KU5+l`mQU$R|xv(skv{d$V>wJ<* za^L?p>4lg!?GPEq`*?0AzPjb&?w$C{jc!dJ2#&P~Ig$W0Js~C#t{N1jnZz_Ld&_#_ z8Pya2B_u~BbDB3v!V{hC#=YNhX}A~f)1B1#0$<*?Uk1hxH>DGD2rKJ?N;rgWGa`Xa zTw>g^ibc%RBd4|#PxFG$Ji?*&y&rmrYagm@oQb==bB$Dl_v(6yXW*;brW79`cr^~j zKPH&!Kz=_72hB-Sqlv!UcV{wit|!+ICE!vHyEhc#aC`rjoW|`MID|WaU)HzETHM{+ zNs3P=c(s1;qYyS&U{l@^dbmT!sC;L*aOY26AY!Ce$weY>id{GtP;an>6B#?@{(AeCp)e+--!Sqp@5%acIX2&K}|m3yL$Hyth~XR8t}qgKb5^Wm;JH-0xr}e@v&Bs8LNjiLQ!uiGEPi0=;RJWaQ>CmG7U z`*o_OzHQ@lDy|m3{2sNSnmK8J+Q7O!GMA1&e6WK_yEIa&dqrC|bdaT@s&=k{9-~He z$8X-$1tPJ|V4Xqco0CqjD8rCOoHo7x9RZpsT_{ zVq0drpn|k4YASyhN$;_P|AX{t{4GA40@xKV6)|5lPM3^l<|`+Nl#EN%Yr-@J0C+|a z!kC}2ho3-y7#Yl~q3>SygLjTTG+x7VqSyDYlq8BZ^_NAbMeF4QLZILoWmw?D?+5JT zyYV{H&+*oCkFN)D^<4X9$=m_XlkwX*HyGNTlf+xJ@pXrUr`4e{lK`glC5!ms@{p=t zUVs#oc8^;ue!Q-Uvrp9HzLRrAG(6seeTsqFVJqU)y48*dr`%3mJ#gcK~S&tc+eQSiz+Z`+~1&C&? zIf-+{L`ns+e;F>X#jq3f>z5v2`)kbOuCTlq>w1+!WWSdsivPQJykrVb+c82M&&_Pn zRito|8}6qZW3y^MMZ9C3vlK2ZW~p_Nyro?tE?$t$sg8I?ay(w%-M|jt<}cou*rI<-dc*oYtpVo05HAk zwMcQvE@c&JFi9!Df_@r$S#}8X$+bbw#j>r@D3Y|P<&?pU_tIU|t;5r~*EA0JJLpF$ zdqQ0COGO0XXyRY8PGWNCe5r;S5uv@HokBmwd9J!eFGsyr zJf<%!o+MvHd!IN?s;AuxDU~p2R~O+VziA(>(G8Mr)AU&vER50|Qoj(mv5zT_^X-ve zHNwgeI@BuNE$Ku8*#pEE z*==cHVXV|iymJdx+$5p|`-sHC?u9bZU&4phow+~VrZGqZGaXagt5z9PSW^`ay1R&F zvIQD$0Zx*ms!BK|x~J$~qZb~PA7AJ${2)_Ucd*;&HVq8*&1OGUsUz+T{zz>(X1U`ZeIjPL)rX6%s{f9LJvcvMe{CaWuVhGOFTy9!b zlKuvVok`cC*bg_V)Mad--(BS&_6kQk^?K`&Y*@?bHaco8S$>DV(M*;_^JYiX>d)2UboOT$p=GuJDpO%vCDqO02O1wJfZc@tjS|E$4BLh8VMbMVMA- zM5mk8j`|H7{wRKGPx|)DAzIGdVnwLrgq7N2)f~>%^mXXC=IVW|Nm!-+V&nJn0j)#b zwRBgN#*(sur(o91^v#w%ul{>(n4B%KDi!p?sh{11R;0 zEtX75YXre`oN~-7usVx!*FMr5OpUNo)<{a^-|>Q5Nj~2i&cQ(rH+Hb=^Iz5_vc4zI zs@ce5MA%gKvw}VUHm+g$+DBIJV$HIS%c_b9tJ}XzS>&LWM2S7bwc((sKYv4QiIA~X zVBRVagiDME`L8_Hh5-H_`$%IRZ=ID|V5MfZZ_pkgbvJ9(XTkc0Ny=AwyDfE!u&q-} z9NC5N)rLFLPoBB@NLj9Zw84x2)k;~LptY^WvX~Ur^v%=(_N*^6#^!CV`K5Q>QdfOf zn-})b;HTkw2I~scx9t=3v-m&9s4Z_@>dxhUt0}R#Z%L>QGlzu< z^#)U?$4BjH1JXW07xq7DLhG|$CUI|5V3!X_Q^#yOklSGX(!4L>kMU^ZkFX@YTm2D_ z6WSDm*nXq7_P-v|ki&l~tVmnwQ|FwfJ4oo(2X7;M`V?7P)3n>}$D*CS2-IHm34 zrX_gmyYa6#sNV874aazT1vM-@3e#;4^F8MJz!@d{b%I^3{}TxW}H z2&OcQQSAf!*-FHu9SBx$$+7Nr%=c-h+C!NY8&5WCnR8bo8yXq!TpR04m`=6>bu`AK zF>33s{o+t~V;@p@za+P7y`VQOqb-ErwGrLK<-J=SP_N?ExXSB-_$zGRTYj+nMk&g> z{@-d___p3IWmL)JPJ~>SHomn)=DE?Mv0XYl;CG!_vdfiG>n{1*Zb?l8`{Ed7=?7Fl zfluq5Z5Sv%)^Sp2NF8bE(Hx0>(C|gg47gZ(S50)KTKtqPc1dPD`~4UN-1)rWG3(ZpIX+;YiwkpApN zd&8iX_)CMYV484D|2R0qJFPDW;X7B`_Z&WCW7FY{CXG?`N36xI<%jq8p+(u@dnTf< z#J?ZlVxq#eJFuAj-jW^~c8Q~7&k@WXn;C7-@zz|C|F_j}IGi+8-acYo*|R-|cM*TZ zU){Bg_%)p0H-R+UyQb>{Dbx|#b(b{JrlI8$#czz#9DL2ZSx(>Q$@rG_bO^|Zj&I$0 zk$xt;qNj_l^)_`1>2k-OPJjBDaZ8%M(T|K#*aH`Z)5;6?+VM|h9T?oi8;Hm3Xyx7v zPw7T+Q@m?B;<)jSuiJaL@^OmB491KxDrNt7MOAsi9yi(PtQ`YM()9SszE;W7@C{vH zae;Sh`w}t7$*V0}#2WX${skj>jPl=~tiMzqxw}OBcUJcb$xZcHd-^vvERP5E;Oao(n>xH}Q@sCbIc3`9 z)Y>eQrHLwfe3U0{QTYD_%Bm)VI0Asc`%aYD39>Lh$E zh8avG)M5|J9#14-yRHqwvT%0~!{N8_aozFA4fsL*7L<$-#6Y7j5=H>Wu@1zP|eqqJw*(jiyXk*aQ3f#M1!75+e`uJAw>NEC_Z zP>rJ2)lKL)p|gVn`ZBlmf+s-OXx{r06k6xrIv#x9+^#H!<{4AS3t`W6KIKypRqDvC z50L|k=77IZk+O64<*0`Ohrb-FhI=mWxdW{15H`OAeQ#Nzh=*|MpOTzm&NWZUJmHq= zz^(HU&vb7C!jU=Jxzlf;T=>1`SQTm=O-A1lp* zeXECTfg?87obYc)=&I$@E0F|#$+_Xmy~m1n(*eZ?C{4w{o4ZS8CU9bZ5di|Z*?qpG z1yXl2-t6VGwauxkgh@Cxjs`bL5;jwN|TID%h_k0$2h zPZjqN{}71rH%Rrw1%98&@kIGl5T%dUdb}L#Lx0rYjeSDr)h)zn=qtr2{6^X{><9d3 z+Hetv&_&a4K2OY{q5SNKk7-d;2FQWbt4A-Pn?%H&(=Z@mpaqZh;(riLz&_#~!PMir zx%&$X@p#T(aaRbw?69M$1v$lTgPy(OrX=4+;Ns1L@w{3`T_?h`5j zqtgWDZ^5FK-LWpXAh~G8J{(9!oV*l&heJQK0wL?5_EaE?Th3IUMY%V8=T)KJ)CMB+ z(dSK_c@#{a0lNu^1!ym?dOwk(dK#3?Xl9q%-cI9l^&Zv4v}Wv3 zxsdbJ+f-IIf%=Uq*|3}DMOAu1X;LbAVhLSARSxc@sQFi#^C(>28?^(4%>7M2Nnvmu z!E%a)vo))ja)zysIz#!x`r_G2jbX7TPN8jPTpoasw=3^AO(Q2OLY4c;$ub&^P0p8` z0O81Vv2SJ-c|>?T@;CXDK<@E};w-S8FhX6$;9K__3_rR}O9%U!fvVH8e(0!N2FqWQhlVlf zs=#6~AJR_D+}tCQY|Qt~ z8RAXY8-dG3T{xlhFNqC~W%Xel)y?dzoI^^I&XkvsDAXm=W26>{jU=0NJ?C$+J=r-< zDjG*FTul|0k`>NW5t$6O_Atkq2|r&x$X+4-C7Z_DN&X_Cvetm7h-Wf~w*L@bWoE~o z6I^0W33x2pDV{V+85$*TEleR#oMZMU5`<@sy8uK%fqq?97k`O%YRnbhLUpcx0XIwa z$>|SwhfFd?EtFKXo)LT&U2l3$xGjvSKU;O1f6_9THJisb&xncQimKyRJ>l##dOMA9 z?#qftDg8dl*Pts;4hQquFr zGt6~#r5?(-gv z8a__FRvuL}Rp}<(y)954C@EU!A+?Bs-b*C|;`|vR=>dUa)Ry>|-nsfK)u=gd_y%LD z1iH(GL`9KiIH^X4QN3T=Bzd4byZpTPy7K4@fW#~WjZqmoL*stxQcY_8MwpFif<;we z)$~m7lNL#D8!xZ*lx#EHUJeq6>KDw|BJLE9%>d$Yx^-PgD2LUb+HXS@N@GiF0aKpY zs7UOW=GLb~oEIOf{k+^w)Kk4=#zN6|VaTX0mY{7;RS0%%(-*+sRgMj=Abj?NT0Hpo zW>Jj;6dAn3v<5b~w4nMSJZEyYxd*;@jGEo@0bYr@+jtcbSJ_qf6uB~+VevvaZT2^H zpneBe8V{pSFI{1b!?;eSSEDfjqZCuyti@MhA{+Sl`pTuXD*WTDx8{q4b#cAb>4e%~ z8$%`G_mY?TGepP9WhWsI_Eil!%{tv|!1+f)=Ah)% zOX_qOTK42_ortCyr5I(69{f1eySimOeEF`LwcM^uOErP>K31#$!O2_WqMN}Pza&Q+ z&6zuSujU@>!YD<*-mpbph^nehlFcvoGZ#xvXF3}7Vp;4Q{cI6G=!Diz_%`#ER zr4841 zl%idz+uN3hq|^*Hzbf;qZf;zi{zKnY*R*N8*1py^@P_JV4SBIad0jho@oc)fvgY~^3IK*|N@r?okhz2iHpe^7fyy@848{=~UoMeE4s z_~lM&InU;89c+ZKKCVO7m9bL72VHwYExNTv=G-EU0q6XdV!-Cb;Vdm~LZ~x~p8XuJTFSLDi`oXtTR= z<5uVTF8STHmn}xwo0b0N2)Ui}O_N5teL|)V$c!GN@;k4Y$5-~X`Bq2dtZw2OGPhi> zx79VQRoA@I?ppEEq|#!Y_f#K~4^I$i)0kyrlwT*cXqx`=9KXh&wSR3Hs>`oA zxt3JpZT`68W%Wq4%K3uvgZ#vVPK}sJ9;JY*_fM$k&p)>(s>&hx;h+`(-{{^y33x2X zr}r7?le=5bdT_nNlD^F#tDbL}h_cq2(X9>edSo4Rb^iI?VbJTzp9i90rj46+;NZ}p z+#WwffIF%y0bz33)%^_q#Ky*a0Pj6Uo!$EsQJ#B1E0`k%^s&x;N49YJatRTB4^LFNwo=1P^!IAC;`F74Fy>nN! zeIR|CQQ7{0WK{sFt7vs&6mfR|ix~ zu62)RkpGYJ9-=9J=g;krlb=YA>~)qgqt%@*QgGn=)@sRp_m<|>(oHj$Hg6X`x3SUR z`yXXL_|dREf8Nd--IL^)p6%Ldef@rtLzjBC6l~A64~Z zP*(dg-+kvCOMG%l_bxLzTH9W2@(6t1GOZfw-q)zsFP)j$$P!JrvC-}Lk2;$ShE&81 zKZW{NVVW+%t^?)rzY(QiHOT>a4=O3kKyQOnwt_K@$mD=7Y#ZvT{c(H%YT$Gp;0+>u zC>!(?S=k7I44`l_3#<~&B|JpfVsxeJky>oimSWToT(y56`Zhje`V%Y~_xL2g@+7%v zKmoW-&a1xdZbB8MQz7d#cjSAXFPL)i@{r1UOLpC-yj`Po2Kmw{gxf34gGzM;>X(gUB% z6(0f??b-Lchg0%n=xOzivTr+8JdcV8-ATxKRX5vEdHKLYP3t#_z#;YPR~!S|m|CYm z;Ip}fBRyzoc~RFy46|~?2*x4+Z@DA59MCW1416@?pFC@}1MJPFG~x`zo)r^__fYfg z=oBXuWnV0E9qe7lb5sQ)!jOZ;AmN+=3>KwFIABreeYrp!0&_10i_gbC@(CqO!pUtv zlXCHM_Z)!x5kc+i5lG^I?h?{S{LYR<4UnQ?2hrzAnw%KSBhsslDC|}8YVT}Z5;@YA zNVrM{@Aih~vrn}eVLRA?+5q?~mX>)9>B9UC#iO<{>D#@~c&0-%2LokTmMbuW3_sf$ zcn&>as2hAi=H222T`f7R-VY;-lNn(6knkQP7V%ZU&R&2D;-_wCMf>t1yzZi7xP(dl zxH1-a5D(HCj7^`we{>L)44SBMr*DKQlsCct@Eh`;EN`TfbTH}xYK~;K*LvhX;><~v z*t@Kj0Vc4zac<*J@Z`E>N+l${W)dwPYBW9p#lc?czGoI8{4|+S>yd7%P|v-HyGr*- zUbqAndDm&cy>4COdeGMPr;4LsMe{c*7IMD97Kno_wG?MGz@es)$Yw;o;iG3f{Dt54MV__{eud3(C?malw*)p9qR$-p~U7788cu<8!986;oWui9>?L1 z2GPV*xTmajy>RlMvd-E>%A(3evRRaLKo(hRO{TA``b~Ln{pWHFH4~P$eiro#V#YEX zS_jf|+*d{=dR$K$@dC8g5=iob&yn0C5fN1+4CxB8t@0x|9Q80QjogEoyDpTn61&KK zBjqI4GH!s@jYo7Z!e7BF%zqOS3G>AxgaLwrxP|CRw5fPRG!e0>6(l#}>$Px_nY4AO zi9DW^K8`|NLC)$N!p&q>nou}B(?b-3cVe8!|G;Y*$ntK27sE3php?CaI6_7Yr#CEl zLM)?aj@v@HK-=9>fzgShsuyE@L{Wmr*nXi0t`8R}uqiX)uJK+bSKuk!f5MOBe{feU ziN`P$b}z6}k~t~O4=Oi<(b7cdH?7Tbt*m$#MHVGl~LZd2k)B_ZMSarZ>u z7w^aEMFTd!s2TK*HaH4jyUO5!o@XB6y+AiqJ7Er5GsMbE&R}k89wgnu(p7z7LhMWB z^2I4wl>Cg%Jt~i`YdwWL*dC@wp-Nhucq>rP8amN`(8$`b5(kX4c}~&_tKNkV{e@j= znCbQwLsWmVIZo}Rb6fL}G5y)PcgP35Laq#z+gXCnL0xMNEk>cSjf})5%+k8vP%?&P z;kx0_t(sVye(D7}p`OM$S~g!*%W3FoV4q zzIkCer}*D>mcA8h%{pZbL0HNb)+X2#CY|{V4neG8S`Z@zhnXPM#4YofIp~Nr2bdI0 zsLKf!3{&y%sz)5I6)|pLuP7=Qe4L1p&xpXUfX`-3z#qszK)*}KNC>50BRpC&iT;te z%_W3kB(D6IqJ~>4==13w@<7@h`cnElS_^#wYz~b;yPJ25Rz^D%Pp4JTx`OaDDowrM z6zx83EaIq%p!NzVvSm~X|2)l(n#}tHc|={oTadH;nte~#u7y@@tqdE2q!l>Ih z)>VvdZRQw?T=_=2j8d-9P^VJDo@wd2Zl=-5*z=f1$!aMVilm8L^ z|G2CXJ%!wEs+4RebBzGXI&zlY59~<}(mcuuBKxUM#s!f>l~Y$Ykyj`@=OakRrT<0r zk4gVDeH0InZZ~9*rKFZx5C}&in33BBB!ux~tc=9cA71T7g6O`_UranA|25jfY6^+g zV-bguN;;vWpQI(NH-LAEkDAtI|0Z6nUlco^_;>AsfF9y8v&DIc@IgK}N~tf%rk5(Y zA0+1T_qcarLe=6*i%0X{N>Fp;}L z^cC_O=OBCxtExCH_yj+Yv0dPSoEEL%t5A`?_xKdF>zooH1=aa4MHx^9hzc=roQ1+f ztOM3ruo-)#{4&1`2T!N)EcgW*uJPvJhkfI@ZwPL4Ch%!^<7f|+D+NQ8f7sLbJE+Uh zA9x3;Ysy}5A5bIG+PHqyxD9@sHfqMo(;O@{efBW7mTEgnDg60cxo222cxl`$)K4ym zbGY;~N6A@~`k8%$&4@BtbI+!)%w;cT?VpWfKVxx6DR~wzLN>xo=O##Zpf+-fB)HP0 zY>wC~bs6i7@Ll8wR*cYQ#YN^XLCtJ8RwX}tl#;b_OZ2-KMh;##56NK*G+`x`tTt6_ z3WWJVA&ksmmdm48z!?9?7tMOibQRAZqZV`a){QWxuy0tji07>PW|!hK%uhy9@)c%? zK4<+fV~6&W&pSrAHg?ty#w^j-QA#?(+0_1(u49+CW+6&hB~6=(Bbg=jOOmr0I*WdN zCSz(%xlc7+TD@i#g1$*~dX(ak_2yDXB0#Gz-wuvZKdi*(-B*sn zy*%(TEb_!OKZP@L)2J=(V$EjME&Ll5AKe7JsJMZ#rc%mNu&!ICOFv^Thxti<;Lsip zlFPU~(-ulk;3%UMXS1r8?1cNR_(|RhI3v#|qjH+9cNXaqwn)U}1EC7Bn*4Z~Nt{i7 zGHslAFJYL1&Rk!_^#K;PWZxwG~o(!ENN@4Prg$jQ&E2q8| zzGu7{wZ+=4h!dZ{B4w?jw^fnS8NyH7oy7vd%XnLnAOBZ~N$A1Pb$=;1zz0peA*kg3 z|2t2&VOguPH8Gg?(mSemm3t)-%93oZs8xP+Gf0>ys|uMbh?eTzYxp~)$EF_RFXF|I z+G6&}uA0wbu+nJLq{?uy&hR#SqVS9EOxz2BO1mIf#usZq?m@igYR9PqyqUc9V^of; zteK5*lALSItvD^7S9d&1A*{5lievM?np1<<@*bPuOW*LqbTv~o+*sb2_C=J}Hsej38S-(uD~S!2m0Tj!wVUPgW$*s_w;q(k$-K`{v!OW+W;g% zYHQAb9ux(cXpjvl!^V85CVHHn4!h{zpi6*XURa?!kFcA#SUVFjcG^w=S?ZD5;5X)I z)RIE8@e}Gv$`btrbms=9_5-HMKStAs`Moe$I|=j8#8lN$T;k}s@JDK95*LA2SMMPf z7o0WViKb+-E}M8e>Xn8;TD0ntdM#<2%U5+JDPSU6ae%yajI!3PFy4arMkHgnz*WD7 zf!%gOJBQ&LRizH1e_K_id`rLWvP=1yetn`;_KR*FrEuKp-GY0-9|n{lG+(9j=VP{o zYo_qJA|I%h^6Xa4R}6FeTx1F}&uL<-6wC4)qh=ZJD6RqN`WpF>d`E4zjGMGuy+XP< z5~7TfB>5eYpAvs^;mXU!jT4Vcyjc;W6n3X^li?+Bi{4hx%DbcK)}Bs6sfsnzBV85c zs;hpbvVT+;UHoNY#kz@C#0yztUkVmvxLUsgI9?~ICFO}UUNw113zcU~G3zhNk5te1 z^O9{bymPUWo>j(9ye>**#g5v7+IEE%t4b!c=a(uo!dnK*S#f}-h{~+c#D+yxU%i6s z=K)=2`!v!3RwYV*30g8rL0FwL0g=VK+gyOM^k2=fp!;!A4Yd%T(2%+!Xqs1KZ2-(= zHlp4aYCSR6`Jw=0)Vy{Ed}nc3iwc2C*EjYfLt^jNPe6?ed2Ja+?e=o1L8AR;D{JSV z^foq{TD-N!hi+})y4Dx?$wfWQ6YyzizZzEJO|igQBK~cNpeB&8!SjykHUT_aSF;-b z*?JR^Ke=>_THYe3fr~;LduYjNjJg}N6`LMfJZTd{0!)8VKY5y~;nWYal_oCLsskvW z{TEk)_BHdk9|~_YG;wih9<|3gk2Y~?rg4&kuUB`lAA7=#%h^|EE2}54t?{zr*#D@q zra>vCP*HzT(vy16@>INIQ@Ghxv?rKr+$(bNj4*hJ5@t6US_RfvUViI;)Uu`-+L?vv z^`7d_sU}OH$`bR@RH)n@9A)^V*y-_HU#!?Wo2l;-TTcPxUjI?Rca3%ChlQDSji%OA zLyg|}HRgJCy&*F=&@fMb-s7hZsC_!SUDqbAv6&_l(kG94qj={Kxk!-lX&1R9HNI|# zyv%uRanIrM!@hgE#FZWkm0dBFLzDBm8!N5qt1265RUZEJe6jzaqViV8@&0Xq@$t$& zM_}k$Sobtgh414I3%G3IyN;>g-IL#UP61n=uW}v^I!YD27?=iqm4Vx_AEu1Y??J*h zt=-s}g0Sz5_JPqGf|Ut2}({biWz86JHLu=6+n>iP>? zymn%H2X?n_d+TD{v_+n}r5b;zzbblDuiH;QChVhx{~`0coBBmdI{8dL{VNVv=8}m(@A(d zI&H->_E#OLKQz<&{D#hik| z2|X)jKyMJIPJWKOP4wM^E8IfQZyPH9OrN3KSO%t@V7;k0NRz;HfN?ZHPAbTmmbmde zc!cKd6AgYueLWeA*h%FMpUKYA9UG*56_v!kS$zl#YdpU%@kQ!7ij((P*ia!yP1 zQ&1TDthWF(pS@u6T;z6Up!=Y(XUP%jCX)5^LFhVOV#@0>-K}(;S44GXO zD-E)!tCc5pUS6*%9%z=@#h`}RF9xP&U*6T(NX^~V7pa_35YyR2-Cfkzx&(N>bW!7k zOtq668EcEVqFrLy{>V_&(?#z+vQK zxFy2@g-5Pl{|#M>GJ0fTW}xvC?h}xhlf40eU8ql%vmh$^o|FVxj2R+*fp%Fd;)!s7 zTv6I=C5-EZVc{Ox5t!{kx56|04`jAXwse&>yXp4$E2I#{i+1Ljkt*WnUO!@&#s2K=*2}&^iSh5Wu)N;mKucu@jZED0h797?RaXrPmxk326GBmn{ zFt_5L;&+5gfXXCWVl`+mR79$P>~}p)j(}eJcLqQfHR92PDv2fslMa+}#$i3c0F(&( z1?pAw26q~EC2=Xf0Rav4_$%}*e5J^ow-2#N zm=P~QZWROv-9mogm%6+~oaS#CrD`A`x?zrNE%<~kjy4zaMiU8n2b-qy%6$X(Qv_|k zjR=wPgSI1%N)aw{c)lcilrkRyYU?jag`kJES=4OEB(nmt2D-?&H#Zm-tzWU(AD*h6 zA9w+NS>xss4qKxjj#24=_nkWFWYD@cB;_DDz3Bs32Pv!vV;sF#C6C^R~HRU1ncg#Q34RD!%7*%B@xzJ+;tEP}#$Wt9FNQ+3# zqux+m!r8GLINdlJ^COmxd%^6jNW`ZyQ#0J~#~J@bYYCef{eD>lB?CM6IU$jeHcBZk zV;aR<*lk!(5f8)0>V%5&Y@Drtl^%|x@>v`H#(m*+`yRvl^L*zraL2e~wv={grbfWJ zg_*8)M-O5sN^+S3^G1F!je-Tsyf;G5~3)m zPxY$O40MV`mYRk>X8IKshzY9(t^{KO4dOW>biX8IloGAvQ}f?ZXYsm9hT7L@{v z;~r9fq}^;GbjDg4n+%gJk7D10%j~al<6+j_ul0Fl3kukFur%@_RwKLt@|qQmxK)5- zenkFAyvN*wo)&SP*@yn&wUhZ3!?UkpJw?AA?IC{~b1v=~@dx85UI!jx;0ZqYY{qIr zYoZOqp6D9k&afjgyjC(+6FclH7#E27qZBWJo=F!Ge$id%4WL7`dvtA{h}KMpZmpz| z=<(sLGz#6<^EORNpJ|^;`%QZ`N^z@bc7kn$VCr@LCD3?kHQzn=5Ve4pxkW+^;p)PE zQCD)Oc&1R7ac}o{}U@PPk3+mdpsNpg4(F zdVD576o*fLL0Qe$j!{b~i;P!rPs#rnVgSA5L0w@Efeh1>B*c?rRXaj2k%N?d9!$~` zWy18+WH0{M8^s)fXc zNrn8e8GyP`@jbr+aZ|1+T2Ka%AxfGvc1v01cAG{dhbzN^+$2bVi`zwMCUBl@lGFhF ze;t5)nnIF2D{n>kNP8=5OLs^N09wX;aU*EsreM)+@Z-Q;A_{bqn^f!t{o58O`T~9T z@3?4p$=#A`kPGl|@llw2$s^HTcu9Ja@GpckMkKUHHU#&q#*Jowi{8y4vk?Iw*k9R;U7_sn>LYW@jkL8;mTLBEbJL^PBv{q9JCr?5eGR@tAFmap=~DujeW)iQrVui1~EE3-(a; zv1BHz$xy!`n0ZL=;m>7y>IN1u7(J?wwiL!(cIhaE=&8P*x1;>M;cH>^_6U7ZasQSi z?a$JG)=t&jtg!QWprKUOIQi)~mB+1-ls(}8eMn1ye>FOkiOVbXaTT!aer_8zjtxo*M+c5lFqJj{^|17UT8r+=TA{MTl*daHGVJVdgFF}8 z>it-D5I4o~uKWhJ%cfooBE*bR&}-^ylDu@DYC7pbCPKj^FWUTpJ0<%Cj$yW9EFZi4yvWQ?am0uo z@+Xd)z`I8u%X5H4D@3ZP#eMR5$~76Yr3A&eIDq()tS@x02ra#|94@SpO>~?uJT9&s zw}5+r{{MQERXuns|5oi#TqvU$zNHUKF6#^9W{MYU`$CbzKFx*YVS=L?uem=3C#C-5 zytotSCq{c%UecVE+nY1H(XGHS@m;-pQF&BHZElGq;DcpSS=HjFmO16ZY5&yiDPLfX z_0*6t6{~D#|l^Jl+^arqi@ki4Ypm6%Onje5))_Z>b zL7o}4Rkou384$C5dhHNsC~G>Go4}r!)&dUl_XsK zC{=2%vqN-bgK8ciiCe2oCsAu7F~$q1&jGUxo6(ZRa{XQO;ptN2d~}BOW}CUB$77U( z1&V){b>Eyu$lh|dI)c!({-Gg~@XEhm*GBYOoT&39227Xg#}R$3S*aIjrctUyUbB*( znT0Sp(TlfW4BzOR*OPT0=?nZbv`Mrdi#;^;v~SZjT37lkYgJ(Xf6-`(r`eacKWl!q zGmo%knf@L3+xq3&+uVv(KQybk4sK7>5N^!$PBofiZ_Q;r_a9Y!#Z(|$k$J|LAgxZg zq+ci*S$AFYOMGipw>n0QbrY$m;-2XOmB+ucE@#RAC{6Wg?b*!zhHmY$grhp^oj~jM zX_l)xS9PcoRF!Uh%3Q_U>H8J0_!es|;s2uS?e2YDIb}%;dQ$Tw(Pdrp3)KbrhFWP1w{nP?}?{PJN27J~g_vWy5^2D%)j}W}ohn2)Tqf}ORXA3NR>%DeQ*sl#)Ev@k3pr=h42#DvshP{Ya zPE#9`5FHcQb$Li@P6cNJ)vC7r+rzBX_FveATZOGmtZ>7`ra#yhLBfV+T%hOfy3aU> z)0FyrocF{-7F+y%t2fsF=fliDZCw=it;sDul!T}&jb=(xP(ghNCDKz_i>3rRxzy?? zP80u_-cqeQC|1G$sNJo#Y~QWk&9m7~QNs-w*72YXb>mrJPqZb773Z|b;>+SqT3@|{ zX?gH~I93(kAy*DMj7a{N;>OAtIM(a^!NuH#In z|4}6^K}zb@>5W9?(x{fY-HMDLKa0D((X-OLN^WrSG<}ggngrCtS#8$5S^9s}F3;k$ z?K>KNmtM%9pbV~9RTM(wRv}8ag6;w>6|q^%!QB9l4L;Bk(0Q+y(3_Auw!`QK=+T`% z`RB@t>k&o9icb0E(%`Bm)Gg)RpqaqkRrZkOnXbSnm|xTy&`*S|mos=avcR?!vj(-H zFDa)FHmoI{*b0^n{a9biSm_iIAZ7;e=)K$$brIE&^T`qq?0&{f4z= zGGkeENM~vqR$5X0HS@W|S$HOAuIK=MDBoTPEI(KHNU%KRX$hP^b?r!*2XFqek`fHp zJPC@u#A@#FO*yZ>VtkR2tsM~jm90>-aHn$h%I#%u3+VFnlx4+prJS|iB}0P@dL0N>Tl4{xbZR~ML$jK)^I9KV>6CgBVm$ZS|kYc%Et4Rk1(1=yY)bXGC3 z^DAVith}u;%d-4?^O5i+AVJgkg|6lCb@RthW-uh^eh1`@;^YPhdZ28*;w%;ed`w@2 zcLu+Ozz8Q`H!}q!AhIvamAoF~>GG8P4tvTxjdvFRp;rV`LKyWyh#uHO`5sggaw*LR zLq|ivLR<`%m$3=|4lf9$5?2u8T}p`-q%?D1zB%P+_ieBer>_-2uMwumLf}7$Ov-lT zAJV0Y<>)t*#p!j}?Nroq5$+d_>)eX-rF}D>!S`Xpx`KdL8OrL*;4G%Uqz+ocJW2+_ zx3jW=zmca|H`BD}K{h00HRdtU<26Ds(b?&Zh}3bwElD8cvm0d5pSFWDVM`G7_Fa ztd$bLV_398oN^qoOx_h7i!73Tb$Sh7ExT@ZjbA0KZU+{BtECzO%63N^TPwyoDA;7{_ zeYlN4PJ2e_xr)OrMM<9^2b(4Y9fGcEcr@=MWO6mrY&-wBklP$g+*tHQdzo~q48%i_ z-vJHS-Bd@gw&Wfy4hBs;Kt~|$1Ku-`XwaN;#%YXc+%~O>I5pnGDZn~S7ydPDDt9Sy zB?5)%B56^b#gEA6F?$mpQzzhd1}vw3B7kOJpdKgO9Hr>T8f55BtcyAd8;v{4vBT{l zaSC;5Be{ploed!`rdE$q?ECeW$nmsBfw!4w4o(JP2x2BOLZ35c)g zZ-UT*VXRW%9T$pwC0OD=7vCnB?~sGH;{WvmOee^3AkURXc^d4F(uT1Z(W@wce?jHS zwfR@juVklU1=tLkm!A^*M7nR*Gb~xUe^kqK7F;D5Y@9BGLE(lGdICJS5)NO0c%{3a zKN)4z@?x%_KWYm6W@6ClHM3eV3st6h>?mctUE$lTl5T{AH+ItWuqpL47#@D8HY~3k ziK~u`@k3px+UQ%2_N`hps{lPy_iw&ogoDz%KS+JSfK>?U52#1G6nYJIrKKfz1i@}{ zSa|?>ts&Rf6*apaH1iQEO@C%gOV4FKDcT@%XXTVxlee+%0hfS(v!!7B9COZ6=%uJu z&KpFzR|NM6s&v|IeijNeO3|%#IkbBfJB8EejnHUP3nK^KT=9!J6?rk+nRyodV#O_1 zE%u^kJ3A2{Jx$4x;XaR2Y-g<##Sy)QzlUm#Wf3{l2Y4sYBib&)s;mHdC29JKxeNdq z;AzWvMTwsl$(lw!IZE-is4o+%DI0khQa^P+!GqjMTMK+j(a{x|nN&6-H4;I~Wc={Z z)Ale?)5g(XGXAxfsv3{suH$AA8hNwuw!{HmVfl7a509MzAh+?FB1|g^yq!yKQHFVk zrgqX6@!G~}L#e`c%eHb7aSvo{+!XvD*(AVGqP0|i{0VVg>OZ zHOUk|+`&4HDXNRZ)MB|cqA~zZWgJLbh}Tz54I3nsR(^ID65K0aPBkNCsTYk=@_Lk8 z$0k-LTF|x`V}ogH$u2#I?QFcA+J|eczYrFW2iC1~pNY4wg-z)rR;m821BkAQG(`Yf zk*KJ&9x*H`Deo+LDdJTyk`9QrLmflLiQVBO*Ygr5#Mg;Wq}_-+qgpJA@FagL=sp$6 zKLG*4FY#Z&wiG!C<|CdY*$P0Y_rXNL3Cs>xgiwk-KJkv|8MbMZ;#moHawnqzlu&K~ z#shYmTaO(noWs3|hbIQ`djn#aBuzk$_DIzgVoo=pA% ztz#pp;|qM*yQp6hN;xjHi_0813ezD_RL&*(xrrNjM277c#S=1A>{Q}6rY-v>M9XAx zlJmWp*Ex;xn^{qudqEo30M~KB9M*oW%S6+OQe4vvM`%i>E0V5?Zxg-g81WtOY5G0! z+B_g5L0lI1h_OaIE$}_lLUhNao$)|)d7^~vEGiqLI47wj4VnO_O;g_nPok;R({g)g z?^H|Tg6Ks`KwufYLvhU|g&wY)I+4sAk(qpfQ7z6=N>24#{29s*<6YCPElQOj*PeRW zU>Vy*3)P~QMMYdO!Q{h$mC-bEknJn@H$GU`cd+>Y@><`xSi3hatAd>1u1_+ z9DP2hQeYl)1nO_F59XiLGZ3>!DK3y&q;v#6L2gnsf^$osDtw`B>0jm7;73*r$X6qG z_~_;L(W!GXmHy~m7TyXh=5L=DNGNDo72*kEjcf&KQ>m@=83vxdP}+(87Wq}m#6x{1 zNIMCpK%lIV&|#4wvnK_Qm5@x7L=o=5J0%lIbBaHUkCKH1qQ);=o^9Q4xOSLLB7yEWs6{ea8kqs{#U_UuTA`Cf=9Ch`5y#N zEp&oCg0Ev*gwOm?r7g6AAEp>CJjCak*NMXE!%n8{uH!Y}S-owDZuS;V~`z zFK%u_Dg@01)yEWE&RSN_!V^uAx7;+<5t%BrF&8*Qv+)bX8HR|uLl79*5WL#L@ z4{)xU0s56~ZCC(V9($@X8QstSiNwXfV24;|A&WLIv5hm=K| zY<^{B>7Jn0#?I3@Vj_cd+A3`A!u#5TIQ-Ody%_t<Biisd(zA*tECSl6a(JXLj20$Ipu~J zp#C?ejZtNH+ur5h$%$z#E^1HM-!ic@KJ;7D55RUWRTB@mcW!d?Y0!2nX-i6lrO89= z!X^KstXggXoU+$67X$MXLK;6-utIAa_Cv^C*$p#bljdeM+QBwk-D$W5dtj>hY2>A2 zR4K4Y293@7)aVHN8Q)&N2eB%2e4P}v*z|YB%GPxYurFwII6;djcJuks!Jn_ zGL5yCl+rl=>TT3{A;U&4b(^O})oPm0oEqai+JXs74W6{yqxJ&dF{+qSvyF8u)2f=s zj*Z=5G{rbWvJKDJhdde#mh3xoNCqGFsR`k_ublH^_G5X`+iHx+KBLaKLKGgGW_T`I z82qZTUii|(SKlK%Gsi_gAbc?)OFK_^_kSp*u}lfd@T&?_hQ~aqe5^PeY|zUU!AoxH z_zJ7p+jJB~;sh^sm-O2IP$51aU_x z(!bis9PY0v^^8BD?JDZ00CM`!}*> zM)u}byovYeu7l{qR<3#ho9%1Y$%nIC>^mnQ3Z|T2bq`@h&naju2H@mY+b_0 z_kPjb#;9^GYP!m3nKIHmz$iB(RbOZ4jj4>~>6CVRUVf~gRl-}fe0B3#-jMgUrVw76 z^WsJTPdnv&<0I2=f5>RfKl4A7TiZBkacoKpMM_#OYT7Cd@NR6JD7oWo(=c7~aLQl< zL&7oJVrUd{{!693YxU4n#x7~D(a4uW8_#JbddnN8srAnGbsyC0r(CO_uZ%OhRf!P( zn`w-(17}K}=MFTk1spBpDnEearOOyu5byF+(0ve%Gd`efh;oCy(v+fmapufSX}y0DAzC%AL2FxUtI zJMI{(g@ow2nR5!Mt4l1%H-&V66!&4%sI_Io_<7&};4Z?QjIH2uk|FdFbT(z?f(_se z)H&nUvQE&!UEeZu$+v3Qxh|CJ(ujgi>LrSE@jKcv&^^Em`mXdw;5kNrsCfmK$y{Iq zvSfZ6SIc_O{>aW{#Mb-_Sw{Jxq5PV?dIzOZVFj=z6 z#k};Rq+=YB?JxFfrzeuC)COkiMZ;uaTjp84f^axjptAvdDR`$DOm#0FQPV@_mPV^8 zox4kDs=9Gtc9J-`O&Y(Zxx2C}#kVm{a5p2W?k!<%_Me*7fWmx%u{}j!gf-j=ZZ6(Y zIpmyQ9Id}Ru7F)8j%mFg2kenoPEC5dDp`=9Zrwf;-<)-(WmegXyxc}|%Cf?!`lR6K zB2=Br{NF`!#y{iI*%)y|s{*_@SE%oX!U~J{pW(HoR`@XFwsK`z1^P(EHjTnvm4dt53-Y(p}VjHs>Xhd~aQ zaX52F1Uen>GTNia7n^KLN)W}GHb5*2&dmgV!jxf7f%&+Ik_hO1f^{Opv`(@+&0!J0?h<&rM4DN1jp0*5@=8oy*r=+CS%a& zdcndOTSqBQUETJ)@$8pMS|NY~WF?oNIeSp^0V}vRMHay4+=6&LcnP*_tBgo zNF4X?=pmS13(Xcwwq|F-qmDhBJ~t`RIZf{$cQb-)OsPJic8e*3u4N=RF~qs%JIs_{sX`n%ADChLFvkW zPrYhU`u2voGEO$GE{?9uJ5rMjuPVG@%*k&p*=DGY)s}s)^zruwF!kKoJAk31r|}C- z$@npi(OEBjl@-!%N&Avp-4X|%P_VkOF@I69vOYQ1yL7l#=Xbo!zUIg5;&QlwK1Nj) zq1NPrC2g3rLO<#U?Awx4Fh{(hJS1-#abX2>m*qgN5%AXSO zg4_xu@hb5*WHotFMJH^SQk3-@afr%{azsIBLN5~P1wDSc5t~S#G)nOkHSuMWnA>^p z%eSylgtZkete+qdq?k?1ng+wNAFoJ6IB>E(&moYU*6BfLHis}unND@HEA9|a-y#5QGRC*$T52g?pyM+(h1XkC9(YN6P)BRC5X&HJO|zB|LQ- z?g(JEYIk`ZP@p`P0j*f4cop#-lB$UDcm{Eo|FPSI7?Q`1QOdG>(`jIwrA4o*-EnrM zwN-k+%W{j#=jr=E!FoG@^-d1vIh(IG`C~LC5XlX zKzZ4P`ibfBz;U&+!oPzmt1o(31K(65?c88*H2DUe-@8X4bUS8aE)55p}+OGrB?If}bbHiZ#F@CS}ViHOpQGbKHw z(@;u^m|_Kgwfr8{73H?bmU;}m)mF#&f*u^DSog$4Gyr;wb^&XSa6lH}B&dL59sV`O zBe{VXjs3HHA*meS<|ZcXCTz5Y(ZqzmaR;7IGzAfe8=?|XlL#RQGxSzsXHhLSn)E(t z4{jaBHH3#hMfGxXz~7^uw9TNDQ#Xy(pc@DY_KXb84K z0|gDPUr;9mT{d@!vju|FtsDro77Vh`0%Ksq!-f3O=Xbl|Knuqr=3v!{=+Wf-WI!G(E1q zuo2BOn@zaqihr{dt_D!pXe0&z`x{)qH!6DSQu3q_XiY`D2lSEgN)Qva*XX$5I22r! zZG*u>75`@1%wMe6Ilpn1Y;Zv(XfnsF_;dDE&Je&OCXtH-Is5%n>PV#-rcGd=} zX(lE#C8|BHkQ#za#ShWKN>>qX)15NL6Q9s;M@5jXFtomnWDaw>V-e{!)75eE&qOZ4R;#VNZ({GLcFy&}d{&*ZY-EN>=KZ5)!b!C{m=rm; zMql&=MXYvBg`p)?Z4rAgMFyd_8Y43R=bS@t)+;TsOojMg3=w-od@YAlAr_y`f0Cah z{#fjtcv2Et7PaBf`Mrre78RfCEk5s5ScUXd?(lOS!q=g+K!LB(yx2&I^EZ z6MT6FBtAsL`-*m7{G6}GAg0#{RG6qy6#{$gd!cYFIO=P82HO(TnUl?y;e6s#*>7m?8V$!HR_4gQsgwQXOE&}ox2J)wD0CN%LQ(QK474>}ZLY6H} zx9AuPL?37OiS?ECcWsk(e~dy1=d&-zHql!-il)ZJccvJ zXm^#dg9941=l`3Z!`G3Ii8hspbCQXh!4LC3pDf*r7rLrsCl{~1t zU7jEDNqHEw+qYBs8a(WDT=fm|)w)~V5Bb}d@PApMS$VsBB?yw|BVP(`NV+O_fUb}5 zlUu^;e8n=q$^2MkM1v*wjz>oxqIG!6IVW8NFmn9_315@w4P%wjef8HX&66QPW z62WFxnLM}T5;YQ405Or6RYj`d498^6u)m;=3!382icq0ZLr1#0s0rmtfq|kxB(Lbm7Cw^)SWs z=BjR(-oi?E5`h@iqCXrfXO5XZ1{R%Ur8@?H7r8{2iWDvN(>_DpTbQRaN1vOXqOV4` zS~#oUV!KD}Iscwt*`bp(z3!79njUKyEJm+^`!~usGVZ-%K+*vEdcMYND_q4PkmN zsBYuAqxL-E7!?n#-;R75eye^G+GA;XT@faFQGe|Ptc}Bux-@L+L|W}s?0VBamSL*j znEhBDccix3ba-bJmn%zu35rQRnv6XwKN}fHcl$G)@#(USOI$Q0YeAiT`TF9yhM!8NzT? zpLwtR1FAB4U)>HFl)UQ>F$N0n*u)&&TRzkDl#KsBl`%F}SY;_44qIe6BmL<2&JZBo zbkF4o`@g({esTRy0vRBgeOtz?bH4EZ>WtSZr7EzKl>7 z3$sdI4V-}Zyi5pcK({-s1qEQ~<{BQ9aJK1s@*408ZFu^A=nn3i>=uM8R-L~R#VK_u z>c{L&(v_-lPlKAuJ`k=uO#!5k+{~MJsnq)oPvax7P&Fob6Yd))ApH_SiE+-}L7H5A zJ->h~OXL@2P~HX}E0)oM<_Su!)2+;VdBH4Az0JyCI#YQv!G^J%wKs*u_=MV%QOO)E z8p$bRl_#|1BiYje9SY#=P4jGuE^<80H}Kqe%34I^fRL}i#9R_$naznBVHxsJ>P=y4 z;hW51;fnY#Ik$ze0advj!Xn4jg&x9w^PRjI!bdeWVRdSfJSBRnO3By|Z>8)*nkTCi zmkPtvs}(cj%d*zWIRU=e%j63j5&0+NM)QNbi6VdFr{z{PQ8HNMej}a!E#{TsFan?0 zP`RvNN9r!!mAIQ3m$X}#9?G1pb#z>sw_bD7{22F_D99KXJhgSRbZ_|k<_UDt%9sW@ z!ZN|SF0}xg5>*3<>q##*K3J+sFE?h*{gQjO@~!!4?kCamYDvl7^w+XjKt+xVy%hMX zz_fiBtS<4(7eT)Pe#Ay194adPGLg~H4YLMOH2B|@J=}#VR(@k)qvUeYu9Dw0pVF=6 zDKJ5~u7aM|Rk0Mxj1fZ6h!o!)PylM%tPD5^eSNeal7}jrvL03>i!JhSkgJr&!X5A| zs71+H;DyRG8jNZw{;aTTn$MXRJE>t7t{_oe`>9->YOH>r z(UECmjE@v&$5%Od-pg8BWp96{9Hoo?r{z@fGGh|^5OhM06Ltf9ae)J%85vsqGQ9xp z3#bb3##{%zaet3(fDBBjCcc0oM=8=~xqI2SLPu5-P*cLdKq`I%ww7_A*`SMQ#c(p@ zP}nJ?2b|^}gH#}Ero`gykbg!gI#P-+0)rA5n@dZCUc3Wp#5GQ_$0%?|Mk%&NyerQig`ztYvM@2o+7c?xwqyptAHOpt3fMtR37t`q zPujfb8|WB$&*beW=GclEf1xNK3qw{?2XjAC1`#U@J*Yd1E|pBBZA>mJYp1s^Pb*Jl zkQN~T5~lTJK58j5Y^*i}FVYuq`Y6Jzdt3*2a9$#pP5g}#5O0!Q$=M>J)`zjeq++WxuDGr%ayjbGl)>~91 ziJKgO>Xg`zY4Pn7Kk0f%7t(CBhoFO*TQwUC7&%|nEs5^=gR1Sp(n1@Rms?1|K^1uN zBGfU(zc?&UoZwQ2ASI>b*1U&0XV_H-=YPm9tzswa%(FE(1TW3ctju0`CqJtaI_Vxd zLb+v3i(8ALWY`m}Fq5-?f_GxKSzCPw1vJv?(U>fQK`po>DsC3Mi36rTau|1;{ zFNa|)^F>_5G?iaR?Ja*;5r?VI4u_C&MKKPrRQ!vjI5>E1+D&3f&F=E$SkF;Pup7G!x*6^O`U*dcoSt_d*@F6# zcpDvz*%O+8k>b*qyv5q##WMjoJbu9J10P2E7mGnZs91i|BGse)A%6J;KY zH-jo%5{=DD9f4vvcR_LQ2E;~|UrVq^eQdX`Dj&m&!(7Fb@0HZYM#fb6> zjmmxqx?J_ad@6sLaQj#Zp%2)T(o2eOvbSe_&6vPZ=80F1<3bB>1yFfuB{vq-@P&ZM zlTHac0H*n#&;{Z1J;oA)au>{FBfxf( zHgng2BS$Ip-}MUq$BK8PzA{N6fczThkouZBy}}UXN1F<{>i?bg79it*v<7v@e?FOyDR#b0-h_>sb1K1!q_q7_IY+{eQmntLb0&|T zj!2+v%(Ftxp>`+jMYqtX5opX&x~uP5ER{awJPvb%{>%0wsgdD4O7Sj^l@QPXt>!Ru zQ{lCo4@r*5KyFz01r(4A_6tQ z2VWqW5FU$26L0cajo2hsJH3QI7jLsw~4DyUepTP7^F za41lyO3Exrip!;|Mk&;?(VRcGkSWN><)%vomkZP}EFq|v6zna0SLV3HUbLY+dDc5g zATZWaC!GlTTZg!JV~PbyoRX4FX?a{{K=euu_c(BH*$N)8;;MTt-vUxK3&Z~b^|Mq9 zx5ECmFU4()%J*Wds`!+;lxc+giY{Q~!yX0|vA!Uh7Ax5T^dsLAhuNX2sI(jMm)U^+GngHFJ=iX4a^kI-B`n0c$RQ7I71|QqEi&iaR6w z;AV$|%5Kb5bJH>xfR8r8zC9aBuLR-MeQiCM0CoTmxdqIM|E@;agx6z_Fv)HIZ0Cxz-} zmR>RWl^LMFwPA|an^>V)icI#s2(faWJhkLm@NRhxAj0#i`~xt2-c7|Nkks~}vJGT< zf=lvXJ4fy5$}uWgC(Z*VMmva)Rb(z(Em;Wh@-&x-p!?>zNSDG^+bX5oUgXr6% z_UwP(E~%OS4jvKZE~rGD2|6hFjSBIYFU&+2I@SsgpugINi$LgKqxQtVPcHu6F|EX% zocp-673tg|JTdS$FPMN_LgMj=u5*9zdWl$Dd%m2g7`3G5kJ*pq313-Dses5@whzrI zu#A0@*6041GoC(fE}8v=Zf|pm8%)QJT5=Nqhsva%<6t9RFvoGu1?*w&=6rPrFkf-a z=6q*LIX`T0Yy$UhZ43U6qW{-k@fWE##JeLD^f>X|00P5B-0mL62o}%fMwT*dTm(;rG0=ON%p|j9$oOSKY;waM!_Bv?vs6EYmj9RhM zI4hsIEX+8k@SfLv?|Dz@g2l?*) zPeh;jneMjYHTI}@{l9-WI5ogj2s6uZ)@Ej= zw)vkw|3CjXePm(!>t^XR{`v&e#A`Oewz5e_C;yqcbXvKcX8Jb!`!j841vnJWX3c4s zyT$Rsyf^a~ITyPq7Ho9AzVOeYg^T0d;Y;KmeV#|Xp88DmUE-JIk6Nk<=nXs=bbHy4 zklD+Zg=UB0!c`GHkq1}Yj{3UNF2*A^Cax@=mQa~EkaQ^dR?3&O>FM4Xv6;nLglu_E zOYX+J@qk#(u!a`Kg>S7(oyjXb_~`ufHj4L5nWz_%0cc;B6N_v5{h`@0^rJ>)zp zd>r^>%9E#04?JsnPI{5_(&^>fSBGBLy`jI&e;4?E>ig#(4t;F-MERWjW&W2BUx&Zd zey{l9@#DqMzF%d(ZGUh6Q!?6f%%6_y8}Dr;ov?4>6B|q0S(D~Xo}E`# zW^X&ga>mb@A7(vv7@2))&Xu{h9q-M%=X7WO4d*K^=NFuGJ-l$Q+xA797O!(3T+-{Y z%Cp0(&AZj7$+y<8%3o*d4e~%qkZ748SP&vuE({fi3Bvgi+(_;UZWJ$Cuu>c&i&e$x z;*ALniA_n($*n2vsa4O>TGdE}L%-)}KH1|y2<@~D!*9vbG-7dac@}Trd*>k|F z@^`>bpsy7_A!g8VFmu=+_)o-l}%W${8Pds;yu!B@^#7;>S@{``VPi= zW)G{GZQ#hbY*Vztv|b`i7p)X8lXyty$)?GE%U>yOC{L)it9v!IT8WON2UTVmBCEWO zj@35R-)o-LUaLD^zq6sQvARjvjB6=rO==5nU(_+9)3Wo^s>fX;-6wkX^ls?u?Qb5a z8dR(ntYNGrtV65^4V7%j+nBj2ZFBOLgst)0;hkm}^RIf2gj|ceo_(YI zCgv9Fw){@b-IjZ+?)N`f`*7oy?FHU(`(B&)8D$h^M1elL+r&GWlv+< zv8L1Pi8GKh%V*^{#Lo_y<1shLF>+qCQ`G!0=O7pF1q)s0ES&0Q>Go~W>%|Y;N0yxO zIN-U}YqfW~PqnYYkLyod3JoX@Ob=SQEI8OJWWn-Tp|)XWVV}caMLdYSvEpLX$>_r? z_rz?A-59qfzBi#Wu_dWKxhBPss!!9Tt20!Y$}DBJDo2^C%v0to3si;bB2BTbq_VWC ztQt^P-e4LRS}WSY9gt3F7pxoJi|9l4qXy7}m_h7n+-m%4!fN6Gsh`|K>7sVfTIr39 zT4ohX%T{p2TrQ8!CkRl&3Q>tTTaqMQAzLQ*QY=t9s3xf`)L%5OwD)x*`qPz%3_GgU z8+)o-YK*n2I$=Gtf!K&{f;5-66u0KL<+NvYWOin*%IeDL&hIJi1@^)F@dM03@oN2= zmbI(b?OK0o=+1@@o2)mxY+1fFdmDDUVn_GRgS+nSG2gpjU+jL^0mZ?=LuU@Z8=iSI z^jP_E@rmA(Cr-UOGwp1^xxDk#3pE!vUOIdE#Z{}3`PYK4r`@QyNxh}G-EgPx?xuTt z?;m||`r-LU7apH`a_Z^fXFH#-dC~k*@rv-e=uO$%(su>#(>_Fg^!zma)924uzHIuc z`v(5*_xdkjxg%;mxHw!sq2Xt(@=e?BHVN@_fNX*F6h+-3*J^i=plr zOF}&sc~0~E<@MbAs?TBH4SsF@+NG=jL|}eU+_HdR*ATnq=F8uO-VeJNemG)FWbcZG zD0MV%B_ReJn;(}PACcgnxG>2f**5u4%7@gaX*bi)WgN-ek+n9vGpE+n57_y*0$^cw zQEc(DlEtO7%dE@3174Kh1fBxzsaOMUhNz%S7#yC5h(Y?J=A&)VUonrc7jS#=gM>Pw zn1m%4P@<_GwCS`T^oNYo%&n|8ww#0K7VuW^7YQZ{J_>J$4vPCFIw@IJC=XY-D900_8~_*!+_c({77rmB`%2dYnO@N1miWZv|=`BKZi*1@*Ac1Z`R z6Syj~E2`VS$F+B6-^9LO{T~LN58hvWW6kBY=hmHEe{|^ZhJzaqY&x*{;Fcp>k8L}> z{o;-rJ0I+NwfpN{%YAnHT@Lskj6Rfe7=DC3TzRzj*xut;PP{uc>9psW#Iuld!t?DH z_FcStdE6EEt7#*YYt7dW-*|It?(Kv-l)Ihx&OWesnEt5o@sp>)&*aZXUV6O}zP|c) z={v*wHy_hK4SuowTJml6_s>7WesO;9`eW(3$7#G|H1RGHl8@iTVXi6F^q@Omp4}P z)@Z$jZ}=0`uE-2NO=+KbKrl&e7P3q;#>C}59Fpdz8yNqmDLqC z#B=Xd&lQ~HWmbMb6!6Y#w`9EGb5-fV3IRe^_9cV^Sv&4d4+q}&;^-hp(Pgt9#5vxE zs{6|IYW&LI$X#E%4UXr)g05oYLNCXcBi?>!UOR_j%2OAQkV6`pA14YxDJz>n~XK=K>B* zWwj62te(w!um@co&W3JAa~Pb-tD_+YIj1`7(yY1b>jQ%#d3!ZmU*{@s@Pn_)6^D5T z_qWRr@rVOj*<)V5F-PXfqj4rk<-B3YVaYrGvb0JGlmBd)arB1Xmyrsd^5m5nCKo7r zA4VpTJ?Mu>cS#Ra&6he#?yxRNY9v137KxKsmueIr63+@65gSAYp0nkN)v!xcS-Ro+ zo}W^Jp3-M4Ijk+Jd?g80GngmDCzN|Cwu(`TaVe+8i)Cp+f#L}g!qXeF%+}u*?n;48 z)ZJ`}zJ5l}YVnimr~1uea+NJ(mw2XrCFqLis0NxcTU4(^2CfomWR_3Lq?-PY^H@o1 zx9L7W@ul{i-KFA?<{F()bg;gbzEi}k-UXT{3a#9jOcTx3zyjSw&T`IUGs(M6splLe zmTPzJI4utAFX{>qb#?h@Iilb;7uqr5>&6+t6ye_L8%Ym^>-Av)JA?-mXCGdcQ1*O1 z^IfdiR&kar$fgbY+1#+Ua4nLXsMM)%b1TV5R4ASez)ta#*PqCdH}b0kvSp!y zcQ-u^o8|RKYxD?48LE|+EVcHh2KHT6ryuTE?X2wD-BahT@Y%dq6e4@K<~9}~z1w}c@RHk~Vw$Dhe;cH{_mu4*XeWcR+QUBgK*%}?Dq6#i4gFy}hDUr*yQ3Y1z` zUT936>L5SMFGdj~xPPj(X{z+X4pDuCWN)ukO}+TCG05m*x_QvP@}g)h>bkB$1j!%P z6pNOw{Hd}Nh4{Xa_lnpjV;b%B3%1AA#cO@Lzg4%YOR9XULX?+zF8c2Z9?C*{UY?Ra zMcpHFUP)CFr8d4ja!2v^6AK#V*Xy^=sIaUZZtPI8VW15TqnJcE*t5houTo_ zD^h(^ev5vuxFJ8_3zM}=%a8r3hj)D5I-zE5Yuu{!M#si|`tOx{>QEdPovJzzk*yIJ z>~i<2aN5(+Y6U=5;p;04l64+^SvN8WGTkHW+HQZC%Pv(^)rK!0Xjt zYtG~bE8iNZ(f+14u`c@jk&eg@4F9NuZrZomrFz}Ef{v?I&I99hEA_{^&a>LIoo#(^ z8+COfBxhJDuD&1jL|&<%>@yir(katzfTRbG+j_Th}CL<+AIPGn$;)BQ$c| zZH_7CT;0Q+l;vqCS~b?SKn`cFXlF$X`zU=DDaKWqD9mr zwQqzMz&6IC!gZOd%1U8xBwo8rc+)#sSuZNyEm<|74IaGNZl%UIylj>!&&st8D-|@# zhuV4atco1tI9XQ42mLi^X{3$DAdz`{E60lu?-F<3s(CqZt}V`3T7RwSROL5WZ9QLi zkYZm0)tm-3ROP82XTbGAitiB{)j#CZyzLcIsol<=&bh6J`k%Gxn_zWMnikXtNjvMV z*DNMSR1Z`|0UsL%^qBN}x*82HqC!nj(!G8uX38RW?Cz-SZR(%j@@o~U&aF|~29n&V zOKuX7l+|8!>w)P8SL5|`x^AZaL4>R7w|bk`bNM!T{PtHJF6&GBKrQOQqFQ|8+#Zr- zS?!IELE;zV;pP|RS1b3`&rF}M?K9ejzg0DB_j}!tBNd+8L)wpSx9&aA;_%@M=fF7;m% ziS<_9QsFu7?~W{C31(kQfY7ys*DzJ+l&q^hAq);(P?;*M_PDFYh(gyf)?n1_Z5DlB zlwWi?U9F~@DHGc%@a4hG?dH|Z#CD%M*Z*H4o^hOOeeyfxeneK?xeoe}&I-)LEF4mD0J)V}z^U}L(;s(K8 zAZLTh)5c$x6KJ((1N> zH>=B=<9dX>{c3%u&#Kw1)>gCD^~g;PZyNIof7Lv$d6am>u*;CXe1%r2QFzW!&Xf17 zwi>A4glsz0F}=vD$z(zFh99U8Y;)IY%*~ z$QzUlr0s$>6?Pxr{8Y8MgSJk_yxfxBzY;m2Az_tuA+sj6c}pVR;8z>C+(A1|FZP_T z=u$jbb#L2G*3oM7ji*>$l8Ci~tnCz9e;wNn(%8jgugTrlmc)_7B{baO%9o{7&*MFD z7ig~uwst<;<|EP>85^Dohs0Ob>=sU<*!6V@`XOGcv;swLQ7b^u7W=OLyWq+)Z)2P= z-+i}sj%Z$I)Yb%5Ox2AI)`}r<=IRl7I$7F#P!%X!fvm#mBJsrQu3ST?Il zBJOv8uOW)T9ox2$4UenhhF0i%L<6g*Yimg_d)})LgIzkFsC;rn&BKZfv7kDVJZ#x` z!!_ykB@5KICCU!(%|{y)hHdMOwKbwSgDB%QQfW_`A+w^lBT)A&XJPX^O>?Y!?HN^c z&{spO{Na)$b%j*d-n==cW04_by=4na*fnsa(SrE8duQ$Lin#V}qbmDy6aW7)be2(3 zwQUr(TR*#7kWK+51VKVV1rd=D5CLfhrf1HaKGWU9%+TG9Ad20|tFPT1uig3a^ZeL9 z&U(%|>skBW_r7E?0pB(2#IxQ0lMV6@TW1Nr68WW$o&B|KvVU7xl^fvD`iRoeBvX}p z@rRV(rIy+^QRT&>l(z!@RX0i-+@8pSg&Ed_pp5HO(%YHXxn(h)kR9G zY%e{Za-^iX_(xQsp;$X3U_iZHG1l#g^o{7IwNVhqy;!ojb5hS|n;Uetj@;l( zvL5#S(o#$%ktJPG35HGjw*g)1>8h!2FQq+_nbysM<-91(>%+g95YKi02WBKg(fy5? zn)R#oFY`#k-uhRp%!t>OZCMS44l+ zWk+~tZEjhK`^OK|jmGeZ)bauJhi|db0TsAZt2dzGt3AX|(RFIYV5H2EJG*bYWC9)S z+$vg-`Cqe-ATK_%){;Le!m{iMx7!yiP9`WW`&2j{y&4y9!nUXb4yv@9h!gviDkgnR zhfw}9)6m3{s^Tqcc8b%(zm-`DxBKQ8n)p60XO$1RYgV5W9l--sj}QDd?;_Z}?+mwT zpW7d3jhPvZLn>w5m+EeLb@;!-OV@{elMPda|G3;y#_*o5{v+B(%uxvrU{zZPuinhE z|Iv=MN13K)`ZfCN!{e@3JE<$ekCptAzxUm#$0QCeuNAii=~ni_&%_DU<^v-eXA#qS zp43dD)wZ2456-l27&HyWov!NCZwv1)QK+u^#^@Kw0$qM63Wcp!QNns|ylTWiMSC~? zp@-5qNGolNs+pN-)!zl9ZSIlsouW%NfuqqJdb1w@t=PZWn zNIoC;U|Q#A9L!`orM&KyGG)>7HU`TlIIDgl+tKr8`C1^tah(1N=k%&7*#pQV2t8{H zr?Ur-r9!lPrvp;(YD!m+3FJp7w30wkaA@rp@UrKPvL%p@<3`;{=##}7=`Z+&V9l9@ zyp?R-(QKkDf5LzYk4@oqYcNu@Tgz_rY_LboOEksvN$EUvrX#HV7x`t;CiOsP37k$Z zksM?Tj_eiH<~`pp7hFuq?&9+|L@#Sjc^ZZ(3j~{pJ(~iaNST2%Cu!VwE zrvlWGZ1ga!D9(G@Un+A{UHeVCyx*oWhH8e4~jXG?rBHZ+wJc z;z_*tI-7l{M@P?l*LPl>mO||~t)+`sgO64mSH1D7GKS06I+d$;dM`u{vNeYkadMfU%M^g}_(V9ZoX`V58~pz&U{LmO$_Z$Ias` z^aA9(>tOcb7jHVS+^{o8ZCRb@^Tvy8Z`4)o#=e9!0DORk1f)ISIKq!M$AHt}Qjd|4 z7o7R#93xIX>UuWgiR9+tQYJ$*)R4qlAe^pz$vVyFu+!Kg-q<~lfnvi{;3ltv|dgcS!c9uVDzxZhC2X?J+Vfa3vj~C+B z3{Js*zsjP8)Wlp}L>s0%94w*}=DW3YhQHypd^2;db_vs(HBMQSI+Z<6stq3n*b58X zEI1Ov{BjIc)n4$=AkC_2#(|}@^EKCM*3xC=7iCV2eA83LGG>9x8^^;NgX@D9x_ukJ=6C8rLAr4uePB5tXE6d(;rt#=uwQ9W^xL`8mF5QdW9V> zU$bU2aD<=v>>0&$vhZ>n)oSp~{^`^s``A@$X=uBhB!ymB|BYs3U==ONAD9irze1B( zUzGb@e*qMM_?Z(EAKns_3b$eKbOK$o#@B*Vx%_R-K7 z;@~bB?2ZdV`r$Ne zr|S-Q5FLMK5VqDGKWzz}RDbFk2?5H0l0+y{*2kwn;gb9!9+W6@N$iCn{-cmt&~Yxs zW-&x>G0s8|12lx8Qxr@?bXPDado7 zK>dXC%^#q=>EH3cUf+6^NG9c;6OJ1 z9w>K?#}|S*m#HE(eE49R&>i+^sN{cwE^Fg?S0OIa${m8zbDtBBpuIac;J+dE#&ftm z)bI2L%ZF_*trl(<_8pij7%OnE$9SiCXEbiyL9Plm5|;=p=lyW2t|B%Mx5p|r+F^8b z&`E+`L540|5Co{J_jmEWDc9Cb=U$LKRJ|b1N;)AL{#n$O{R3YkII^Pw19(RR%;+tm z)oFP8kL@~-@Uu$r{!HFVqg%~s?g-rvWd|`%eFIFxT@)9x=VFBPX3P)tuIO06GBlFU zbbOEegP%B;z&lu%)fd5KRe4l*5wT?x6-{`m@jng|)9CJHg`h7~7h-my+hr#I4&=IM zo?|DH%bk2SgSVz5a9;Bk(<~!%aq5c6~Yf7urxs&zy|THoI=mLU`J0|H;Tw zS*_y^_yT|7nMqvg@$<9D&z3!y&PE_HnYu=or*@)UD+!^4wYwaABXrE+BO`A zA1L^aZm@>`_~?GkH|pK?GpY_+QE7ofPMJi1686PDGMxO5aJ4uneHyU0akCDE%+b!%+H$_ z0wp-u5fpgJ;W$+VA8M^o6!12f*|K=aq*N)cw*c~7S-&YNOVXkpwU|sRykTi>k;?H0igp#^M0+H`8-oxyaJsPPryvXhf+qdCwHLx7Kh|rPJ0M@K=IA z_QQpEg!I4`xxTrjv0BQgpK2hBX;mrQo5HHn9*Q;pk8$R1f=AZw-0DfZP~@(U!BMfh zeIho8s~jkmyY$>>_#vIu9;-hpp5EBabr5>ij3@K?+OqhRzr1Mgej_3)2>5pFMa zv=2fr@JIs-WcLQYHWW!t^l|l$qC*|G2(92r)4bumKmY3J6czVG31thH*s6>1d4PSC z-LrQ^HGHe?X{GC_u2p+X*3=8CRKr>t3wf_irw5VjRh0~{q+t02W>SP%I)hc`?IE%U z0&Sc5OTq13<4bHfzbY$@vpK!W7QGcmiUevBIKl!@S;gttRVn)bj*OTjaRsxzt_V}0 z$+l)*I`pP~emGq7So4{3u(1mWG4kEOeO{M^KOK zx0P`ZBRL&5CL395d4s`H0?ChP{Y3*1TLlVd=iQM%=9?0Oq)|MFu-~F~V!+EqFa{5^ z#kel$srCqCxPDpLaeb7ABD zEr|#t=C|%IcC2TXM(NzEy`)*{n6i%?ONH6gk#j*dQBRFmi3RG_p~nTkWlf$a4;Oy7 z&B8x$C$z3GTxkDOazJ~f>6PS)>O<{8K&9}g;OA_TR++cQuM#`z$A_{6)k>ac0?$r- z%ytjnz#VF7Fd+T3l3*>r3z2MBRkp+dGv$x!7H40RMwUN_J1e?ll!gumRyC76ow*;R zp|(jliN~lNS=UcFtOKiVQxkam%9qln(2Zs)?crWe!v*@&9mli_n2UqUl|d|q$9-ua zU~BCp_ycaLajiYZnWX(#c^|mQ^C(*a^wJla$iV50ZvAD>TlWaHjXA6iLCyqSmWvUV2SeP5 zL|CWsg@||6iRz1z1WiWyFHs56Ug9epL2W2D@TKW{b<=pkF>6&E?)0ES*(LmzM}+7v z)@se-eL#t-G1W6P+3K2dKjk<4jhT>FQ=Sh0kJ>ZMM^mF%yFE;~K)NpQiWC!Bc}x)A z;C;3>@kZiJ6&EX?RBKhjvU%lI*cH;U6O@O@=?rMC9eqj&Q2B5aN&nvnR^R5Ra%Msr#9}RS5+y+!# z3O{r`In3sIwwfihL8j|hwrWLe!Rdxif?I6Ins|O{Ze00E-lAQpW*tQ z>Iy#2bx=BlEnD3x;G-creai*aXn{|Ilf0j0s@6$$IVa1Bgc^Z#H8y6G+M#a#ZudX-MjT2=K`-IWtlmah1e$TMD%1#Et&a~CW8 zUMe|)>#m)W65e#HQG6jDu5E0tE`P_5uOC+u#5_`!U0j#_z4W?nN#Z(Vn98tupSDFl z+3&pKsW{uUO|qEZZ?%GNOU%=TGzZtOvv%kD0XGQLmvTzo`l*bHjD zRO|d26`1s}YpZy_(ACPDcbhn(`PB5QZ8z^y-R4Fc=K4xg^_A?xlFwz_gu&u;W7Ot& z&0XzmKfS_DesWmF6y;iN<(at=k{JWrD2t(ro=23jqPgujH7YHwX&!Cu4pR+8-@fTp zSqr1ld!Sg)vUPG+uL4F{jFg0e&f>xS3^odJdPcGX3g5L!*xqTLjZ@hLJMyY?+4nY` zC^Z9wccVeXiE!GcS_Q^hYw_dM-w>y!Pez4f$yvPi{5c~J#3J!u-yGHWe3;kMz+?#u(^`nUXm}`~!_@zzB z<_>(MH=*mr{G5aeDr&M^CVY?15WU@}ksk!nPFty-^sKowuIHwBv2^_Ob6DDJGM7rMhx_zKq2c&*SDsLXC3{U-7%aEOpNhrT7N!m2c zWF!3RovjVwo1A*&4qPA0Qo&>Fizujc)2thuj-Vw}q(6gxsb z8BJ0Ug@c6JiuvhUt04I$)yn~znMx@-!lD(8CB+| z-xzYExLhUo&Q|{;z3cQ&`by}udN%(RaT%FhrMosu=N0_&D;vqk;Uj6w-m^-2S~MdWHx}f=yc_ zPTA5_Ux9T65EZ@HoV9a}98_puqI!o~t~w-MgWQ9E9*LH=QW6Gi#g%*8x~~Z@CQNDF zA#m7IR#(I83lNl_=ccc9Dy|~t+yAR_#-3U%7U|J{;oFDRnq`#m{V7UtZ$tMO`NV{& zEr{gEma^J^Mfw1B*)IXMHq7vpM|Bvl6ccQVOwnAd0X}f(yD^orp}$|hYHveVv}SC= ztmX^K>09b+?PTr&Rb}zww6*DmjRKv+8s$jt9gA9F6P^f{9sj zvNXY=sl(8<<#f#)O;Nzb(p`%6Ym4=JB(oeCiuHmCmZO9-i1equw03glY}_(QYzdf(uik+0($V(62x;sC661`3e4d_=LP!AUyw`lEq)R zkDx-_V-*~lhNu(U(L3>0>LBAGb~ZVUwHkdG>c-X}pVyE$5%A*&)`h>+l5=awtCSOZ zu26zy)n&h_g_0cMby|}sn^H#qDj+9un6bR9O)%>L;p6%mXhl2jsY%aES!ZgBW*I+r zhm-yF{8BN6p$QPksK=D^$RfI}Y|^d@Muq5V$Su}JezGeIu)ve<`jcig#GH;R^sBzs zHL57Te0Rwfa+7%+pF|y_KUBn{(bYiWHO4sk{E%SgfM|#70(LX;=#E`MUiX4iVI+NP zb;sMnBMr;V{^U_r^*o4@UK&!kpZe5rHPMQWs4as}F~g*DTu!q?c;2_S<$oOPKS?7k z>f78gqtLgb$COyaY+AsrB0s2>7LKNpNv7G!w4hcOHSIBvf&JWxN>JgH&0;0lQLnW*Z1!XXh z^;VYEn8J3KJjCfH7=wMi_?>a1X68T> zb4i7B-4HXcBtVTZuNTL|qgZKL&M>cgy3!Quz`h|Z**FV0B@A^^avl=9FZ^b>w?*vl zWUv}JwbvLYYs4xk)2&<$SuxL=YO)Wra9#BdJ$r{TFQ5^K72j}*=Wj zj1gT8wG$X2EiK9cMr)lB+|3NHkY+Dn%`)Zfn9q8z9TTvP{X=Hsw3_40Yd_yg&pwva zpGa>yP*amkf8L{3PGAJJ#&h}@_vyZBc~Nj>FLxQ4CZ`bvl-l7FyFh)Hc@OudC&pM}?TnZHJZvLtz>$r92fm-U%?)8a z>2@PP_Ogmv{4_gWM!{{^)$H?_5tx)Y5?cX?wkM-qoaGzdpc$Ooj#j7_c=mWF{s2GI zH3E0QN0nD#2o@lXM?a$_tYIbsx^VBJVJEFAx)eEs1a8=knBk`mcM&O!9gD}RrP2-q z`bqq_Y$X~e@{vqIZVPIer3l3L$go1zaW8Cph>RwZ{O-bUuwZka;{T!?87e@1SVe{S8j{$&W!#B}(%=)_wkk96dneDGZGkI70=<*g)aQ#f_ z(?X^0n5_hTS3S|cQvM`I9zdK$qp7vqu0$w%4xPCM@k|ndNA;tqA_kprNrl!tb zVq(73jTO&kZ6wwTYgsKtUj<=oj}#5>7yHhZTy7gs>?0tk9KIchZv;DfS(5MQ+1fGU zJ*c1Nr|<#NgWC%TB)n)c-y8m(e4aN3KDhZDaS0atEWumgY`b_&1#j=k6dxClsJCY6!wxoP6eBwr#yxHX~--^O3!y&Fp+PT4I-$(To1yl84kXytu@ zz_?j?ioaa%i8yd?X%-ix5IxF~yVQ8I^g)CL)+?&_o`c@tyV*@dN8ov#N+GQ_rgA?2 zYb8(dmnSY2!`a+mBaq)g*yyr%O~h?f!Qq3LtMrH0bu>emY5N5!CxDJG!clFTD(HNE zW0hh%Z$V8Tbci@tUXwo?Z#O9tVeF7@Z}={BNIBW70-YdEvVDeZz9ORgEvh7V|7H?T&yx~y6e$y(Q zUEw707WK)ZDE5#NAy;I-l5eEWh`T2pMXL{8DC%KktQ!VvvdA{$c%y;W4JtjvK#Yaj zk&I^|nW~n#lf6)p&b*uLC39loasJ{z?C?!$;Ss=o-6lTH39uQ@bq5XgM|B$!`(j9a z9X=@RP@-@W%S*l$UYJ!RwShkFG>9idPd3dIJc1ss{ln{rzS~S7AlRzzs@6`>HC(`b zoX-7{VKA>b3#f4b1}}^LEdOoYBLePhp1}5XzY~XdZY4^ zd>8+KTqZ4KP^1}ReD7QFR$)zSiZG6UHN>62llx&UpSuhHZZipwL2uXEs*8&6>7FPT z>RtGghustsy;stvtl#S+s+2w6aY@iDSrUAaHz4$18%12 ztn?8tUKVN!rB9PA*PAnHMGMq#cI*_`C|rU;p1Wk*T5BRn;A=A(gYd7_UsaUGYHfqU zvNo4{QhKZ61C1kYEZLs1Tv%i1h9=Gs5vB+jJ`j$&KpRb%e-11*t z+D)p2znesbZwikX$mFn8H*GZK~&>y{}lWF@bz;6DH!n?1@u(JN(E zB@K*eFv-YaND6)QDNL)~otgmV({0O?i-$vAZ>4wG=iK~7T+X=FXZaIA*kmX>4IPta zo25_|bh0=FT1k@XBA^kw7pT3!zflzm3us}0M)Cy;c3UFsfRwA>@isv}jSovZcqb%g z6UglaUm6mKeFaywq4<>)vT74PEo!-ZBepZZSF#$dTJuxz8To1TKW;g)-MF&kqO42& zz*r*r4%+Ks@uq^=8nUo5`JOUIFeb85#^5phKZ&!5yKC+XB5+SDTkZ^0P)w9e(6x&L ziXUnMITGCo<%N7u-7DXo9INP%+=<*I-6zWQ?-up*pRGB~@8)i@+Cbg9fpAMs1)d z-te|E3n*SQ3DfX zqEBmzKpSdIMGoi7-pwVsoR4wKic`Qj;bXP_VEp<##SgH_m=KF-1oyi^?FOUxteb!s4#qv}2+ zr1G>9BUhE4m4~MvFrJq#ioK*eFZvKRS9L^i&gZqPhF9q<78Mb>mMy#$n3vR{*24G` zepu;jSW5OSb#@wp}&BV6Mw6F%cDwjAQ_!)HjY)f}i? z05?~hELD)lmFzPPrOzv_(EW^Et1VMIhMJX4@?@V@sYc9omJ0#C-trB14}M&tswrxi z31?RnRbMSSSQ1z6mv*6ey@|Kuhjy**UTC2*P_@=aEA^5Joy9^I!Ijlxxyy)He1A`H zegk87J63Qr=S0(~!qrJHYTp)Vqh3|qqqqc(F{`M5*VXE$&_COMQm$jIUs)lx0)}v; zJD)a}5!Pm)F>)Fk57H+jov%4hKN)qYypypn@Utn0d1GCZ_7!`PgQda>Ft3^;at5RE zh_1~XZ~Bi`4)8uFv!NKckVI5h1J9$>Wpa*F;3Z=Wr(|7?<{`M+VT*hXcxx3yxD#r{ zM|WD|j&x&7BDOjEdp(Fsk{qf;$gildQVh`tRuu0*Hm@_PKOr+53S zdH7SALEqS%DJaP{)k*llyUtY>^TtI@D3KG-0%-;ces`T*y?pq#CYF|<_buk~{~|xJ zxOS@IHSKFtqwHyRbnO|*lU>}3E258)XU%7YvjU^__52O%_^LE+fkTz#8(wa)jn6`# zWB+S=qA#KKH_g?K$X-}$r;6OQrNT*G5veoVN=FBJ=%H;)AKm}XA9un7rMhpCwEM0g^SjQh14gKpZe~rctAb9=8S0`{fbSU`ahP- z@*fffaGE)7FpFA{r|Wx6J-qu_*BV;Xj+HGT^rd0Rb+Z{(zPHMa%)M?Si?6fJ+Ky8l z0SZ@0#C*_~IsKq7urv?Z*T=rN``?bg?Cu>?nkTYfgvHcc2e$b>Ez1U8yIC8$IhD3C z%05uEa*C)Dk}^jh7>&yF5_=J(Z+Ch7Y2<$~&l<166=6Qr?Qov&lhSSQPPZU^8ysh= zS6qa_l}zD1xR3E{|7ZSxd44@)o@O`P*3aD?GuUvNhzfJ4s>XACpO(a7HEt=o)96dv zA;lySaliT$f(fq4$y2Fc#tl-BDaLrhow6Tzdfm6gZ&A-=E8Ozu%Pmi9Lh zZZ}rG4STX`m7obNW7PE%n%q2#t{19tyGgB!<=U9GdP}Km*z(G$q9?weOlJf&ZYu2- zUZI_<>;Mt9DplZx4VO3fPc`n&TiLZspS_#hf@mUR+Uqc-N7&+ue3_f?XOp+s&#g*x zS`cTqLl(dltkUrBU_Th_zOM4sc~+e-&5GSX^B=>Zn4a2in&)8_<&Tt3eq)XMqyo1t zO`PznU7qwgPq6ABUxhPoRu{g@vprEmj>UlzXLbs$iaQ3&gwikuKV=g`c&37X?glkyL!7U{&KA~}JcD)N?&&HInC zLS%~bqb(K84m{2X{>QbX~Z zT!*3#t!wNZ%1y<_jTh-2;xWz%%!}NWm-v})dtV^l%(tq!3%wxC zE}xpiD6%s;#Kut4G!r%!&@M>X&Q?q+FZ$x2O!<+~13-54es$fZoUdKSRQvOqn@>XP z3iLJSv+ow}D;?Z1MEVFx}Z*F&+aMQ z4y^eSJEj2wwrju^jt&TQ@&uo8RHvTPQ20*I6M86&RCzMS!?yC1jC0US z;0v=98q6Ba8fH1ic(I2EjQ$I1<*MI)_^Pm+=70-iJb zIAeGkl{tpV!@q2&u*K$XfjFvV4X29XTG)pNx53V(V~$-Nmgr(o$$UP~E02n$mN_e7@**n@Lzs zKG9%BFQEKa*^;)47GYiyRYAX?jrLp5BxDv2?d*J>{6GrI&e_&<2VI)yXA~f}3nmMa zkQUM@S~D`dVZP@S!k|D=Gms*h$d`vm7w#&O{NtD$+|jRr9^4m@Gl3BCv0cpl(e zu#(aT{sbFS7emXz<6FC+c<||ZIWz#xwhx9wq0qj6IL&-Yy$7e7N9dk%s<=YpE2oQ4 zkjc5w!Hd+dfl4R)!gm36)9P0dW!<8nxSn)Sha9{tH)WcXS* zf}O8jl00sh>Fu&P9bib8`pn^M5FD}V1s(7|JyTfWE$gZ?Sn>7$sb{d#ssquDEPh!A zDVp`bh$fNPIa*=F7hsYi#rt32mgv4+8Rr^zWOq69b#H9dQ|7M@TNR7>qltr5v1SjK z`yOHCl&N=pXZR@jbAdtf zd>h{QqE(4yTpP-&@K_>>7V5PDmoxU;{KQ_fO)Wx!lKiqXn-5W5$ae8?>TOWXEu~$_ z+e)0GKS+2)j9^X->%;w7VV*ZIGkd?yGi(+ouUW;P1pF=8#GA!gB~9R3a(p=oVh1NR zw*Xgj3<>twQ_hnx2P_cu_C(P^u*&9NbQ`q13FTIEC!4K_ej-I;Gwhy~1A2TZ_AEz> z6=H$$IC=}M4{b+xqEkKHkuOM=%?YFqZg1Q}K+-=Zd%Rep7CT|*#go}$Y_6~{yBTHi zZ^sRyqj@o*s}LaB8At{>qbcgUYlHA!g&uhVtn!-ZF4 z@nQbmBr+dDqndwWdfcqeM6m0xi9geEl7JQlFzPm8PjXAypcS8(z=k2MvDFdL21^)G@x?3 z&+x|7{J;iYAjjk`CJuocJm{79?=W9 z?}?8a9}^0^%H1B{h;6iLLw_Kr%;zOH)q}D+v7_=I_@+=QKTt>rMo6WpV|gNR;x;+g zS!lbFz#sEoy8XtQ2-K<{Wuc$VHWDZEDCsYeus8vB5Iolvk?!*2)Tz5k+}DbTZ3_v5 z)NZ2>zFGLe?G|Ro6IdNVr(@qt_2Sm*#ZszhdU+3I7G#<~k+$(J8@{Lf$IZ}uj;bM6 zDjo*B#_maa+zz6L1<_WgkUsn$(^heG^DOBUVQ<}QXq8}mm4CriUV3Rn3POA;j*5!L zyELHz%@{0S?WRY`!hfyKBV>Z7eq^@D`pvslyesE1%RzT4Z!qVYW@ACguI0*(LhY6q z`E>F=zxR>^+DMo0LIcBSX~#Rw)~RERcZ$~Vb{d??>zM|viLxbUvw9|#lvt&}Xsw&y zO8;Zb^s5mEGnFn61v++w90 zk$AoDUj8~kfeUxICj(wR13Q7?@|d2KF{cJG77EVDUCpSU4`6 zF01vm;T;owabXg2{>s%BST4R*zDskV&JS}^B~@{$d*zqQ!uJMCgN$e6Y(@8Vq2Ymo za^>OmKY4qk4la9$4FY(z8)k`r;=?t$S+zh^#kU-!pt7_&Z&xbK6j~4xGgg15Ffv4= zu^|&)<%$gILq~53#E`FAA%NK`{>neL$zCEe=Um z{iK_{aQQ~2uj5v60ekT(=CIG{!Yiwq#Q4C@DQjQ`<)1SLG8=c-8Sb!Lx5sKTST{oE zDetl?yi#QA0N626#NzN)o#54hO}x1k$KWv5`O@6s#-in>nb6?w<@ydt8~sqT8R7?@ zP+W&(UTdWxP^DwKunanDF@gI69>pcfJ9!G0Pstu`Yo5qBoj98!)^*~qqM7O_d}%OU z{tip>nl33s%N%nB_mQU-8@R!UD|c+!1L+0k5wlLRJ#S92zvyVneC=bQb@WUX#+L`L zmoMi2@%$=oCGs4}f_2!CMFBB@dJ`384w|RTMAIwP#oQ)?S|Ld8)OyHlw+$$7h))NP zl`(|ro-f6tc)uM%el@|j&=TR;Vj{A%$8?QpZ=w|2pButt-J67w~Gl2sqgKD$B1hyzyGj&=P_!jYV1fMxR_y@_wQz3|xlIo(s&1rR23qaeQM@o-@_7Q!? z7%+On3+)L|<$gdp6?|fsA#sJauaxm4p&>S@F@^{wji~j(g=ry`6R>}F?kefQtRh&& zVW@t?AH61-;B7H&Cum{nT8RsXgiyxK`C+@mUcuDTOjZ{jPa zgkRNLNCP&+ss==Wd);s{?U3Cyp@}2 z=7k^Cp$c@v4%M>ZbXljgPN=YZA>6>TSyfDY#zzz)eU+KFGa9?cWGfSnx4GozMV@K+ zn@U1=lhfyJnJ3C0uD8Rro&g zaeV`MOVInO36w|P>q{(YO)l>GaSVg?Bt;AB*z#CWEN5z=b#DcYoB?;7p`A|XZ*HSU zM4qeL%9s`Os-lxQ$$N|GF3ZJbk9LSnvrd+4falAv35UU0h2wiR0v9tP+6#b+1ZGnL zV2G@&9Ss}}8Y-u7mU(YAc5sX?gk}JAvaXSt!N)6t1ahdBbhmppHalZ=TL4N*h-!2~ z?nhFpZy|ew4wpqE^SvXBYvAWDmFg4lQR_$2V{q?^O8#BApR}*bUa&1=R_j*2M?ye@ zJ8wqho~j4L=b*OI6#SicjG-4F>vB-_3bVDDC!K&Mt$4@Zgw&FBolIFu#^e@}X&6wK!PPHN- zpl(2ZC^D`xSsD~nYJMTQ?Y&)}C?H&(D$95qZF0oVh?y(Xc-hz-QeKC)_(sOO<|6(2 zgw1vK8kfk#ij#_8L6zoc>22>6-6hc#mv>4l{u3LH80XGgDd8=~j2FuD=4bra|2+S7 z*7~|`QgcqB`ejj4o(b-z%q+N&yMubOXx`4{^g#*~xP*C+ZsmL$h-3MlJDA;7c)UM5 z_i)kU+LZie$~e_Ql8EXIRTbsa_U5Eg_R>3JLx=g@GdEHg7g;ATd||DlZdv=hzgOFk04@!k6e^Of_XEIyxYT_7_?2)XV$|?>JeT9;sk= z7w)Z*i7I-so=8e$K<-k}e)i}50>Sgl2+||I_4bEkDmU8yFl_@K?^MLxi7q;koVM8b zZcM;CP?@n;Ua% z;qI?}ui8JQrF3m8U64`Q5-Yl%`Mhp8{*#kaF+M#n-^O@pTW#SSP2vVW$_A;}@esX@ z7k>z){LOG{l~Ml;|9GF!{$_s`F&HEBoEbvqv;uN^4$HCd(l&sdMTzt~2K-B(?eGsM zXN3=16`dlnTM+W!!Yign)K%oALYx*ne72Qz3iUvmnQ@SA8+DOo$!PHlVjp9rI!HOK zz|I4$q|2<*rk6!ltPe&qIh(yfU`=Ud4}0OXvB2m(m2@TmMU7`p;*9a_XZ^!5Icx(S zgPR7d3+Ca5#yAp+{V}{Kyn`k2ZjslchpCa2e$+d)jTVgzL`E_uBR;+*(7oBet)$00cO~zac7(2=fU4pWY|;<;cXCKknKXCz$YCP%ovqVpIRd}+6o#3bZGW0Q z72B|{CWl`!x%PS9+>+Z`G+$}V#kHhmx;sUT!t3hIDY4`MdFvJx^_2MS`qhj%{P*_t zEHf_a{hEEPQD5tpySJ9FIh^;Paxr$LAg=^194vG;JW1{&+iE1E|7mRU5*q-_HUECq#X=iWb?NeCpP~js%H|%*O;xUsh8`xd zv1c(qb4xm|a#o~QRZ=;Dnf5AA4w5ZE7jrJCa4Br1789?lMFf!Ut%LAWdr$Etcwn|YqX)a2%g&6#u>9+>2e37Tu^~n1JMy2kMd&%& zC+lqV3M;Xy731eeYc<%+g10;|+FD4VpFk6fzU&3j`4oQaROAWGE%+sJnEq?+D&!FJ zyLAro9Pq7dLk`h{G?@sAF_8-)s~G!fhu{~?f{aplkma-^9&Toj4$g!d*iSuf!RLX; z!>YO9$OlEVsReHkUJV;odXRb z^UJiH4n?Ojk)x2iV1*p2lt%u<$rkI(KPl z8^Dd?ZHf}$h<*y%4b*CAWM806*}g{u9Fg6R{>+&q85ww-lOvq${s)}MeQ2Enp2WDN zVSro3DaAOzqI5qx8yIKWSX9COr5EoR4Opm8MUw$o{vj|McrO{@ex0M|Ul~^A;Ovs; z>_d%HmSBeg-fCZLJm-#f zrC=4fkpG=O9SmWw=1l>Ub8m2`gM~>m2}e-2H4IMx_icEJDZrPmztKmK|;!yniZj{-cK4I#)&Lh9OwFtPtrbQWw;t!*2& zUUl2u(%m2(Qi61M4>L@6$LclRbb}(G*tt#Y?(XicZRf}L8=hmub6@9uUYA`EbOAQ3 z{+Q}#Q@47%YGT8`($C7db#i(?zQ{VRECl13>eC;g=M8gW#-j0BYrrz(wd#(G1sT96 z)Gt+4cA@G3rL6s-lsXimUZUG#^^H*JPV{n}eL5RWH1CPtfgClY1n3c|X27KbNyXpQ zy;i>K4_BQ~PU?9gd52Hz`bz7=rnhY<%|$btNa>EqvAP-2I8tod7QjMmblxug$S~!A z(XDwTX&qwIaGqi(D6YFq!)xwYpEDj6jWWMt7pBZLKHz?dT&6!G*zen+-6Ou|cv`(( zPBfSsrObnHZ9SV^#XnFh;ry;CwrF_$g>@#2fRcRB&@4)er07~CxxS5><+4_%Nvdst zz5Z$aO<^x|xc0qB$Dd*yE#{Ecnw=$t!v7g#q>SVeJwaxTbk~CNGrlJE3LwBKNqGs} zt+%Nw1j@k|RsiVYxtdo3SBPJXm%(WTG{bnXBI%_r5!@5;OannKzKH4|)aN8qPJ;b) z`L(l@BS3e{QoN7LFez|V!R zt7{kIx6s=DiPcISYz^SVmWi84+`pNx>S6xvc)C?Dd>A4yA(C-kPI{J1;1H^*1g62k zodE)(;6ocO%%z4m9}{Jierp^dCS>aByd<^po|bg!qmWW#sXW7Lv5u&C;$T$s!49aa zeVe>pklZ?5{vSotL{+4fa_cuLwq?$?egp#J?wdV<6Cn|XT#)0Hsg**04p&sY&|hd& zn;1R7-_(2`)so*eMxk*fx9d#Emy9iz8_4dsCX)wZ4VkB>A#|@Y%>a_+I8FH(i2%V? zNPCAL*z`d=jVx`*)Hsw-YnQ2QGt$lPRW@-o#u%kr$QWHS9_mF?f5u`R6O{xs1Dw{f z-g1_AtjWX7B#*Dx8PAjqv)(dHNdIgO&?Uur8;lxt$O!E{)iE!bx)}fM$i<&wVZdO^ z!lp)Eb7NP-s+wDM@9Hv&k6Jw}+tPQLK$Ao4Tm2h-OYrp}Y3WQaM0HDrI@aMz>^A^3 zZ|!vB6*hXc_184k37UiFvF?pMrl*@~f)8mP>SueQDo}mMaXlV{cM4w( zWD%d!Gy5KqqbjR<_|)Zjwq55LHc30%-m>;aahh&$tpR)Mx&=nJ{pKX`4Lb+jH+i*C zv?_?9*p3QQ>ue6juY@T1EkT+`0PS-~oGz zW;}FB5VPV73{dwkYlbQ1?>lF~nK^G-`EdBsw~hb8u~9Q>-@z3Dug#NSm3x!k3EpeJ zO`Q+_ApEZxv>5HE@RX8nup2zYH;gxR@o)lpHi z{a4i`gYwih|1~hIr;@T)aKRN{-xsjADZyU8`fCWjet)^u@ZN`s6C2% zIC!g~&Aqumhj#PC?X-2J^a zT4{ACR$jwe`JT)DTCPxjcOGvVEcb15X&9BWpsC9GYiUGXzj;FBR?A33V89ncg;wM- zN^?`S(*aPba9{dAo39b+mA}?MuNlaHuzDwTV(P|zCwhAn<`*}sdN*;Tu^rv(dF>%P+k}Gud{m8- z|33WtFgZTHGAk!DF*z+^*oc3`To*TsN#3~at~J4udU+k&R@IoBmY)v z>xv*@NNinKr077%sn(eixzCRJW3p@)%uG}i*lyQ(f+U*L`tOplm6@w=N$~v2ezA03 zDr5O^>AhHS#{=2oTYlV&hv6tNcT*H9S=M`-WIv;7eTjhJUbX`x$BmT2E!K@wLpac-Q~lbRvIp zkH|PbbzsT!sa;70necm_LlBo)DZPCN{8v7*ma4eQuvU+dB@`^A0)pa-W zXD`2Q)uvAFbT$2sjclP9@Q_>eD|Al2Y34KPd#*w23`2uw?rG^B_9K2<&3Yb5253GNSioAIcm;S{$cso=+mNSz*HYpRs-e^(yE5|pS5%% zj!bHMNe+fjYwu7W!A;^E#sDlMu4h|eN_GZsIb0l_D*OiL`lLwv;I!T6DsJeFtDh2v z7@Jlesa@-=5mU(OPars{0<9u`@-JFA`Bhj%W%A#}qyKJz4h&;`5D@`{Ev0|QmW zy5^R6Vv?1qE+BiFBZTfWKjUP=b0$gmEb|ZNv}W64p+KT4_5LVfWBYbW%DlV!SFNv% zZC~GG@)Yv+ELI|VQ{45jT%rMj~cH}?Mj`uz<{-X5Tv8i;)D)!3v z6@*@6lYx-CjH+BsO6zduw^J0&&xSeypVW6}j$vaK@2J~+vR>@HO?0ZevZ=i5kvkN#M4?{ZAXhef>m1f} zx~#W>eU`~>n9CKibFjPoIb2_!t>7GgW@UtEm2gISmt>_lATm$(LaOr`1we}6&8_r( zLUzwDX1|DBw~j3o=b<`of+Ugii|;8-tVj`t%1Y86iWzb^;+FKj;)v&c#ZVLBrU}#y zP~dVV{a+}t){WT@r6GyzRw$c&kGm17DX-zbgw$!_qI76ogh28My63rH{uG|Ep`H9v z^I+LT+62u!%SFaqbtLqc<)O-9-Q`p&*<}{qF}x*pnJ^#Q6){`lf}Zp2lgA(@)^lp0 zTDa>aWwYhJc@OP^c@FrE@!c5Ce9unOmzG}Pp439Aa|MXHHT&`HMN_T6mVj#?AjjEF6oTA$9lsJBrsUJa)|OdNWDOhmiIGO7PAy9*j-6afI;5=(2dYUQIIvCK9|3arJt|_GMRPZ7= zxA!LZ65!DU@}~k?4OtKmoRzE-Lf|-xP;?K>F7%WT!QQ1&vT@MxklpgP5Z%2BoC4ih zQO9vqWi&={rOJnDf8G&gu2?6SkFO^;2%GTn1$=P=#!uWWoq=8r5z6gRKlfsQhDdu< ztYcI{cOoxIQb;F|qIW{AH0i=uSR^u$z2j0y+|k@4{(?zxJa=+osJ znCqHL>j`YT#(ZTBM^*nn;Wh3btFdMhf0cQBeu7YCKoj1JjoQZ{W2JZ1Htrb;BJR9= zGUM{Hg>}1GnVn1U9qjFGT>=^O7w|r?oB4zLANY{-KSDng%CpG{f%*h_@s6;EXiXpj-<8I?PC$+- zJX+r?TzDE|uR_7U5A9K$6}oVK00AOO)eT^ccx!eG7%QC>w*&kjvjk>97ZpCP3*jfg z&z5ttV0pAbB3m~!2Q|v=6=|F~@@~a}su_xCAT0ZTieJF~xDwzBm=ov>J_0|xdcn3( zfAa=O3wB2jNS(1=K%I0G#%52EC1L+mrpSk({aNMmr)XO2Q^gbH!=ks~Y^2UL3=$#k zP5Z{Hu1*wHVl? z6j_=z?cdaT z;>@R1C#X}f%_<*yjA)0lpIJejrSxJeivQxfxT{ky;6?l&iznkFMNGf**afMb(^c${ z!rt@(-_OlP*Wv}d{lY8QD}g;F5;F3zI#w2NZXsXvhwr->(C?4C2nN=p=}5 z_<>|0D7+h)i!|_`zz-2sO(VPosVG_iHy{xyEO;4W9~B1gLB{#j!++t=&WT7Re9zzp z&(&InNVSie&-`%ckj9bp0%})#7BZn)l~3|}XuUEt@+tHjkMR@0X;`o`0X~J=>0xM| zl?|1Ee=W|u2Jo`UK*|8O84eWO0e9=(Cgb1>?d-^nkiR-?$keo18SN~Av#=oD4=}G~ z7I+1WYg)jY1kS17K|Bw9tDROr1;?7JljnkIhSiZQuvh!iFCPk2|KqHKUgI{p7T{2~ z5@Z7ZcHHK60NU0*ViiDaT2L?n5Y%att^xb-AWOyGnAcH? zBzsJmG$rGhaX4dLWrBVO>t0Tcj=^sz?aEfC-b_I$>>UBKG zPn4~tLVOyoDT7+eRHJ8NHa zK=s(_1C92i7lq;V|D#+@9^tS`=lsT0@m(NzBbV$la{O z#y;wi>Wm>3?AL;awJcU-YJg=Cr#sqe{J{4Lg!C(g_dI56X_A}v)6|KIiQ=z~4_FVF zt@ZCXHH3q;uek3Esw`LewJDcOTZAdmMFz9jEs(FPkve)rYEtC6_AFHr&@0YpuoHe~ zJgkcqy&#OR=83Q8Uo;m;E~OBR+0y&b)AXtGF@eR}C5m{De03VI!hW}s1x*+A*R2GG zF=DJY076xb`8%*BKf*Ky_?`UMFasAL;1bdFS_u0^ z-)Y&4ovVCddW0$Rju^gRQ<7WtlhD}cL>5~AQEA_RjoTo zk2hb|jjxm#e`_b?6&q%#pCw1>f>nnWQ#2Ii>OfCbH{R}{RSs<^IuNlslrD_4T(C0f zW6kasx5{W^ibQLv2j|298vXR=x0Oz)|IR2QB7?1qnV{lAEs5 z4w$+c$5wnc?5u0cy`kG_xt4TEbKT^yc)Z$K4=?(nRBK*)wBvJCIS&2kCF~X}u7^{x zktpr%B_PEm9al-jjHj(PDb-8TntJG(unYCEtcSjzEhl(1mvhEcQJn1qnu*Y4pR@LOOm6?VDW>VHc1+G;fi6(gAb z-Qi4Ab!&%;y|8F)>srpijEbfKo-Y1Ry-L6hJ7Ucgp??0R36f2&1l>9L4BM~joq(RP ztrHOVRZnfZE(|ML*fLd=mj0{JK`e>iS~pj+H>}4pT;}K(X1FU~=Gv$|0>s)Cs;+=9 z8KE7+WQPbHtxS1J;ku>`^3&<2`csOmc(L`8;%yjYx(W38h3T(=bk{-6T`0kBKs6n9 zpr35Jilh>vTGEi5!lK4HBs@K)ZZk3?KG<>?{uM?wUV?x5Me1H5V_olRW+C(K-YB_< zADz|uLfu>SUo%$g;AkFj0<4BeFX**oJe z(3y1CRuAL8s?AMn4TlP@*1y-&(vH?n)ZU9bX7<)pgv~URt6uqqXq8H{n~mxY?rEQc z1L$SiyOy51%~dO#9BPvbPSqD%BGNWntBkkfj+jLH)-VtKI<3GjOY=!haSK*)lqL3J z+!^CmuU&JpVqP)0N>FtzE2Q@kaWL`1vJd3*QM8Ue`rN?KmW8ZN&u#S*Ub^EC^D$Av z!bW|bEWdi@YAfkLvF}O;@`J2VD=0L-#KG<|#)c?f`#5%WU}ck;m+JYe_NTDKiE5%s zR@iv!X34)123Or>I2B#)6EgQ?9$CJhElo6Z?%>j+%3CFTO(3gry6}pZzqLaW<8;L6 zCA(p>Q9A+<5V$Mn@d}E-6?^!COxm)Uf`P=`4oA^HQ4!5Q#C3tJ`fbuEuR=?ue7N&e z!%W3fTRY86@Cd=NuTCN@3hjxN+Go0V^++!zPH)>OYlw1g>XSo(%sNn^_7aQ7sMX^=dR_!;_q~ zx*Qm@J*Qd$zpq-cf{e=xUoM-8$7a0m5MdV*?zc2xnUOymn$QPITx~wAPKkV5e_G`mNVQy3dU)+I+`<<- z-`7sX3hkCCSD|oKanBrcZ{ek`4JJ*-!?wSM2ML#ka-m_7|JB=T0|RB25H;>~(vYQm zKp!HF;9L5Oylh4h=^q^8c{HsT9IcUm@dGW};vs zM;3KkTq>Y?^W@_skGAnE={0$MP&J9Nv*CVCJbgC4mTJQk@qRLE9N}J~%@;*hdNSqWACaq$b;(Vn^G1#-{YGgZswjmUT6E5#U252Zw5stBfk0UXn9 z*hhgQkuv@Z5c3`;Aws1aV++^dhnF>%Cg4k~RTX=%18{e>FBZdYs5yGXOm-im~*f0BR6G|*jIY%wNTqRL(LSE2fPfxcQ!AxncP)gZ_q(Y8!*VuiNp7BjM1Q_m`8k{=_xNnSmnb522I-*waU;&00ZfVO;J=Wxam!l{;)(uri3 z28WcB^g|YP#AMDk{kkEX@VRR3>f7`QC2QM%Fn(7A8O5v*gf1YG^Op1HjdXc7=y`h?!+4An9 za88nfkd(mZ0Rf@MMK3^G50z{!26*H5)=xG#v_wG;G@YU21nX3p9t_DV?E3QCrEZ;5>l-T)+A5X3gt%s^$c>cKkX6H=tf&pk z-^z$EFGw_S#_L~(+!c6ej2<--m2%W_Vo7n|VExQ;pvO%)uS(t(AzVf@x4Dx(lV3M2 z$SGm}gFXYv0@Q65)GD3VS25lK?Uft?u57x{5JLWMyvu2uivi7jAA=^28uAXBDjR>X* zUgypd*^363OqP^N27+MOec5q0Phd6R+P0Bq5xbZQ7)r@JIE_V-eCPIaa;0YpFpni$ zo0BHkD({HL#BPetpit>!V7J>M#U1c#D}jQ*3yd>qt#Ar7kI@4ca&)Y1a8}hi&SyA1 zdl{bx7sXE&&4Fn_!=2^u|X8UXUbVGu>k3jp)o~4vA?U)Q6F`TJnJs>MPS))=NgP;a+6~YlnVP zmYFNkM#NPMYSe_l4dRtbmRo{+4p!Q9f|%bLsjIJ9+dM|Gh4QumV5QR&YY$YcV&1d- zFKZHK(6}USBLAdL6lf7UsZqCN*(RLVR9vlJ&eqK(z35^nd?;RRUzsOpy5_WsFs66? zvdmo^8_WCHL;OUeS75nlP@CYEE~``))vcEiO0?>9+3kucG6y+KkT5)ktZk>uHUs0S zz8Uj?&x~Es&%xguaex;bBdBv8Kn_ZMYn#Ol#3ib938CgcsiQQEYM>{}>=`ztU*)!} zed#*IVlEo324DdrU^Mtk-0gf0-Xiz2N`<2tkCZb-Q&`6&HsTqac3OeNjYlnABrOoc zr+t;J5rss%D;y=s{!f4#GK=$j=nyd3lEB+4^Y4~_hyHVM2BY9SlLOmcm4v+D)GL1oCvtx&W#nGI2)|zRS2!CF zOwAR&#ab5INj{^0{b6}HTHz7~tU$IHOPFU(0OHQhGx7xmoHGXdnxDK9-O3`GV6N6J z<$-9DdS%oTiMPthpCN0;O)e>b6Z+V2fN`)<3|m=+^&9wu?3=av8Wxvt@hyBZbVlb= z*ur8xGpb!&ryb#6D4VKMxD+Y&U{4Ks^dFrr@Iq#5TR%URZQHzx^og^(fiiT3YOZxi zaT4q?-Ha*~PtmLWeWhzP^IeDv8opco9%Yp%6&M;?{)e88BNIC(ja7Yz&R6e{4?xdrXlxBUi8`Ke0*m&YZ*9#y|wB;u#DT8`yY6m-zkI`la13h zgvH3q)O9|wiZ@EYsSaF@kr1X}d&@gfps=~=F|Aru+>lr9DR!;}vaU(o&C8bbO4)`Z zVN+x`wasXakZ+O?wek%n!@}YOH>*; z{{_8O&Js-Xbi@ye0sBF`R_-o4tQt?-&Pi1EG4_zsl)oMUNs@p9hN*a`S( zp>xm)Y*4K748odZ3mnE_7R4Ru1MD3ykX?$c<+l*4uu8$lqEXmXQAS!CdQrSHW;NO; zjSOl-b+Rj-QK&`H=#Y-C2kFu=sDreVWk!C;NY(AgG5M^*1f*U*n0filhMxQvV72Z?N*_1`(?#pRUFyN0h0siu zmuDrk5;r*ff=kfXqT|574W}4d;99+J)ehif?WlY!u+?lzsRB+J=SP=<3w2wAhJj{H zzGop6p?vE&6CR1J;EL)hN^Hn$tvAcLWh{%J>SXp7^C(hq;!dNSaya6g{t-j%FW04W zq^>tKrvyiATh%;i31`3>NT?#uFyALlEPZU!k&kAl8J%c#i9-E)CK$0v7shG!S8I;* zZ@3Nh28wI!{!uNKJF>r+{!o#ceB-qt=0DBQ!CaelSf97!owlcefWc5V(wD8}m$%?uDWy(2# zpBsorftB|E!%(P}`B=LPOeYo(@eCV^sp=QtyG(D@1IT8{YvltdKm6%XH)4-}8Ey;x za%;xeaEW~kdK3PaS)x(lAFD5`590faUaPL*#TkRjo7mMQb@*$nD0~Y(1%2wz#u8E7 zZ8JKEF0kjJ*~mu5TXl#qq{&|*!bhv8hwHIqQ#eN5(!Mtnkw)+<^FMHNHw(Lo6Ytyr;*yJ}2-Nbz{bL$)^)L^nj$4K)oG(O{0 zI*-&B3Xa)~(fpED*EqB;tpdv!&Cx{nT%X2yH6N1u>P}N{Mt`;zFWh(sH)fFJd6cQP%IQtI=D{%eg7R1;!=(m)_mF z7oxS!y_&Vse%oQH6^f0dQH}Fh+e<_1Q#n~VvucaEUz5&QqWIgQNv3f^b8vwEidf^l zTH7mabH1&vm0!0dDmR1kiGun<;i8g_wLPNb>^94OaaB^0>0gOF+R3m?x-odNu2w$6 zd#y&JAi0cDbpQ`-x8skY((3oMFXeqDQPxyNcy@?cq}Y-4KcfWjT>Mba1vUo#)$+kS z?~Q5$^gkCj<#A}A?Qc93=2v6Z1IUZwhn668Sk?;@1-Z9$uYra1FJ7(_BT&$FjR|3U z?^bO=*e*%R83@fT1RFpqt352|)bESU=3KQhtKDc;9aswLTa?!pv$d;~!-D$NSMVtB z0o6ZPoeLEQ(K~izEEshnJh99%UMl98RvNx#!G=eA$j_R2A zX(dz1b5Y@=@u_wO^d@?ww7$2j=zDI~@`f`1lqX&Nm9=q6?KGlGSV!}H@;86i1~0~V z_ev{+ljcxi928tyFjPq!0Fb#$1j$&RtqKdKI3 z?|1JtU*cCeoHHyHhc8^Loh+YK!dTW*vn{8h^Ec&bihcVGdT8v9=Bv!TVHFJ|4$D8* zdY_-}e$7-ON_F(seUnNS9#ju)PnV4AYGPc;{;y*c%aFXG)t^%u%Way@%L((ZJ1F4! zM_P!Yt?s{!W2BQD<=P8!gH537Bk--buj3T&d3Ix4uD~U^qS-9u$2vB^qBCK$YSScn z{^90n(jOl4^>^g!9CvFj0%Ds6l?~)ooY)}omQ24Zs=8$ zT=~mCL2tr;dk8iE;xnCyDlaV1R)vp3X=~xKNBN$e0hPatT}&$C)bg2N8riSvC$pO- zBt0uV%zR0Gmm0xsWVuC55w`MndySF)5I=!IG$wP0HOA zu(BNgS-6UnjVC09(o9%gSRTg?z2kXO_yTq48Ax@v3L12oKh4)v=knT39%6ZMj6p;` zQ(>b!TsV&yubsN|E_I!{Ak2X+RBAjA32E5!<%g4-TQ}Ap%rG}QsqAyp8!*wsqA9gE z$xF(oTS5xDt80v=rAo?A-MP^3tf%T%o`b?+_>1M9N!%W^J|tby^;NknC$+sy#4WUM zK2r0nETSQ_pop;A+PQQIrP}y7^dyU~{o%P&utxcF*%jh{`9XD;$+Y5dWis_yxvOX{ zLrNH3!()4q-sbo4rcl2p`U>4xAt9cUM&4ETQbmzuT(@VHU&WK!6~xJepLjye{hATN zNa`Ny2+~%@AY)wqIQD0@f5IVt4G#-fn*PYiv&*;YIlxQ2bQ)`3+E5HS|^vBS8RZ1S5Sbt zTvPS`02yIN%`MuxQCbb_RGmcp?7_Bz6}{F2T;NZh7~VbiIN2BpMwk55 zKV(0t_^K0DEhhSD$7VND)~h??*-WO&FX$K#!MC^r;_aAYb9J_=$zPwIzp=pqXe{1d zdzYPFe%^vtZmIS%d1cQdU)5iVpU)W7a)VZK|5LqnhsEi5e$$;SdY46aGH+%33Se5% zV2gltsq9&!M`b3Vr1oXjd9uB^Kkf#@-H;h1=i-{n?x^UyGN-YW5tLt`ZD5TmE>O6# zUzSI)Msc4JW>ruGBS}9q2SjPKF|oOl!>r^*V!4U;!Q}?nBk`<%N?B1s*HqK45v=mD z%zsELn3LFPl#S)B+)eZonPUaX>`^g~#M60y6k*?DfEsEUQF$5u(pzJ2a@E%DT z$e#p-wC}X7!h@x^nXAM>>DxJ%B>Q6i5d_Mr173+ADg0eM%esmeG9SE@@KW|l zOeS5CTd9vIrxnhnbLd|c`_t#LDF8FNm;Vpw5^zZL8T{tDSUwiIYqpdfMIU21l^@Yt zA_?&ybS?ECvOh{GDW(}whqP9fC-NdXhc_D88L&*`ju>1sWelX;G*I$KPeTADu- zCW2a9ODQB>*073qQOniIY4%Jvm3#CIu7`3$fL;h<4_(Wp&(Sr;@5R=-waDeNAGOnj zQ>xM|zU0HiZ6?>^MU*Ooed-!UzHY(d6C6-8BS0v4r~K?nlY-b$BU}WuH6q2O-&;Zi z*2>~WF*&9BVx79^Jh{%&o=T$k7&kB0vtR4>2c!xx^}nussVlxoKZgHrJ_4l)?iTZS z&qVI!nWXb#Gr_SiQ98b6Y;wPBEp2Y3Tp?l+e6N8j{!}LvzARa)-O0(U_#YU=y+QEh zKH)P-Da6mhNtEh>CeazXKG{*Sopmj8$Ry0m@jVE*h#Z`@L)&Bp8ju-CRw&-HCs7}9 z?sD!jqKRX8ds)r-HwAk+zmwWUZ~0inb!m#o!*_%HwRF1EQ*ehOMjcG6W8oJHEZLEApGplY!gd2<30`XNkY8mlh{= zX02nu(!T^>)=}AsJT5m<-n!Jlf3E-{!bGP5wy#q99F#i;01Kg3{0HeJOp&gqRKUH= z)3k%|_9_xH1m2u`hJ7qK=dHHn+K+wra8#|3Y&=e`sP6Pw{om48J;Fg4+d#Z!_^Qks4; zaEgslTp3O~qd%Opow-9dJu#8Kpbj z#*+i;C>6)4?=07HN*QlVDT$*w!TN*Y8T^gfP~R{yN43n^AYX*ffiB8{{BGeW#qeT~ z+OF7Go?T`c%7L7-?7;Y%Dee- z85GuVKb0hk4)Hf7Kpb8AL0lO!O}-NF z!2i-03DY6}*mdF(2o2sOwL*8i2=X6roFf3z;Z@R2jO|JxyM$G*OdtZB z68uJy2hR&nP1_)F$M(l&ipsEHDhw=y(p8i|GZYF=Kuk?H=p)g5yQ5gAteAHa*h{ifr-6}_S>?~b zwY1J$S7lHuoc87TI0ydUrr+=JZRy9HISiGRXqyx)d zq_t|#SDlHg(M~4a3{BRg)9idssGC^rF8`?h@;NrERJ9VE>Z7$QV^!v9R#&#>ztDJ8 zUr1Z1ZYSHs%~B7inM2)G9G0)oE#)ijZ< zE0S-(MRIbC0;VfMLUP~+z}9Cwd>fqU@&HMIX4%n@J@7=57$iYp>2B~8bTMZ$G#vVw z%!2IUsWDDa7VH<|2DQMIKIfrNaJTCySO?#>>qI<}NyMLkoAO>M9oVYeki!FhDm{`H zg9~s)%vdlVdk``j+=rF=Y=xrGm#(v+cPPW|7`zR6LTpob82(o}S#eUInX?dZ)Xhu! z4rFWAM&AM&)%n5i!AYuFKAXUP{H3cCB*nJdJ%NCb!SC^6h zzjvvM5IM>Ok<5!`7>-kac%0JDVzoG?Yhk`>p^IjL#9A@d`m+?z|7tl}u{~X47FUm4 zvdJ{Mh8uC*Afx@WXq@gP^SZ|uEs1;4akcu7Xu`q`s#CK3@**>(dS-rti9|Y-_R1JV z!Iy;V-_ffhK%I$23V5&e;Wm0sQQr_ebNsAYDPh{UDEs6e%QhK)QtR^C4R-V$Y4Lg+ z=BxPQ+Mk@@2v5y%-tvIc>UN=z=R%cSeAUTMsgSL(F%8jTHDwcZH#sBoM(A9)J*nHZ z3H*fkR86elL-;FokZ5nf8da!drKgiJQMSy9fJ+n`Z60Cg!S$sg?RXI(SD}H#K<;oIAPWmYPI^Y8^~hnTTWA2X(Y6_#Fyypcq)gHz<)BJe^{W&a-mm^A zt`a+^{1je|{!~s1ScaD1>7D>`59@RKjci6=+72RXQQy4V9qK&CbVFN7;kAUHEq0|F z7TY$iuiPK(Qul{6%csg3MBU_?U}CWVyLbB2eEs~3T1e8CH>xeNgr0V{CAWNKLVlA6 zVO&&i{Z3L>(2Ls1RH_fbT+U>=stnt>jrIgxwD9W!mc~w&lM6QQt7=F~Yy3qlS#qQP zH#sUQtac?WCupt3o@w$aF}8Akx^CCo3RLz7HMb=Z3qGpe$sgp7ZOkFBNIg-%f%5?J|%L2Ay9rUXuK|1vDzm~lL#z#i%i3-?a(QC!bKY2u^SdRqldUJKVvu;J-FzEF1~To@#>e_LPanWjGl3 z$qiWke?s<-? zxbl5PE#id)Y591vriMz;6da|sksX(k+2iP6LM8m0TyKwKlAq#H%iOY07I)XB=ie;% z!ygpCuG%GhT``9gUjtSHl*{=qDUJ+P;$G%N4kdIRuSl@c{L~t_XICq$2yvH@sH~FRxNixpLH$Th( zIR8Vg<`!{x@$-t3_f10XN zt0X62EUQD>&YPEiU&g9tm6GMDxga505t%TZQVK+eEMZ*&=^l@SbZD^EBknCCHqK4j zfYdG1TNW54nZPAW-@;uxgO62XAoKQ!4w5%=bByv(}hKR`nG8FnH$JmbdECmUxjsO?B{f#yVAn$2xv)N5&@veOB?Cz&6(;U5{!`l$2v0mQ`s?7{E|_`YkEfTZbqPn?(vbYRT7#>Nlk0M=`%pjg5=PJdB;f!AAhv6>;!XKbUSiVU$cm@LVfz$v_^^096T2?+FC#pxnZ zh;l)WpEz2wsNk$5o_3&AA&oB^TSb+nXH2i@l`~`3GHevxi=J|y1K-`u;scP(;+SHF zBC*ez%@9u2X-bMwo5&B_ZR}Yh zZ*D>kq6?I?a&xrPSmR{o`&ijEuagqLY z<+tJhcy{%Zvg7>Pnp;&RH5%%3;@hGrOeachikj2KD2);eayUi)!zGo1f1N)l+NB0v zUg?MGiQvl0T+($OSUo^lMye%GrBe$tX{%V#$v;?1Zg7+nuTxOy_ewloe9~osyhGlq zIariUKMVMl%^32ZOt0L}K1Y-jLGGD?DvE@EB{_+)S@Wc@1n2wW$$Ri=OedEbaP!Mdar>L%!ak@H!HAgy1$Up? z9}?%Ep$UOjY?a6LFguoyO~YgM|^_Bk;7xc{N784 z$2A1z#?2=F9J(TCEIBWt-7N^dF(%n^F72NQHoI=4K1%UeUxxKVbSjGzmMwxFz^~7SJ=q-`sf(oMbm$Z^*(2_J(0z8x&pP!T)5g)oOr6Stf%M5-v&cXhRv~LrA4_rVpQ+V54 zFiTQztVze+Ll!Lk7jXcKLpa7RjNKg<6Q3KO9`a-2(?p{uJ|!~wxm_as1>EBRF0BhO zyln}p5*f9IgdN1}TIxhNf!~A`81G6_u z&wdQdNTP0SO7kJLt|>yVBE4LC3U`>iE2TQPh;cD`Ti9bpW}sP=8U41~o!C73RvUEU z*EIV6B`Jf{%Ud%M0;=B{8Hz*cU-|+UMA?$EJ7_si8`U4G<+cY*NB+Qxa^uIa*(Yr_ zB&=YC?z^Az9^>iOnA9h9=^8%jN1FAr?{V)@K;k5J)b0BSdmPJagI^Lp+%ME+HNh&V z%2GjGAHHJGCer%osZCyF`-HaNc2Pv|FBXw$A0Q10v8YA9Ug4JLeE}DJ1(@Ivnll|s zjo5Cv5vo)BWcMw?2MJ$pvL~jd*!&ht{su9<=mvCuKxzCTgepW9wmyAPc%e@-k{cy- z>O(zxW#s4IW{3~6$~3a8~SVN{LU}rv;63Ot^X+LLVFH(L{dO69hUBuunvhF67Bv^MpV}C- zJMI=$@3}D%PYtv`ll+Qu&kPAKrWEhmlNL`2+}MFKr)>D`A@(5o`I1D!0&-_mD)gEB zOW;JTf=%-nPVi-s?O93Bq0{Pi!Ay+3yM9C{>3cV7kV=~4Z+9^R)Q}}yd=rHnPN$x7 zJ?Hm;de`%@+jq3Zers%w&>Vtj2Lfox;h%4RpC*o;UgtzRnQ(l0INbui{nsga53)bZ zf)wv#@_V0jI>5!%hHM}F+-3(^8{V`30p;W9`fXDbT>R|10O~(eb}iRZYY=A^y3^oj zWvB)I*Whg5&+zBMN?hoK?8rvzEaHZk8~e$`i3E#nLJ}`|aGgInFcrD{JjDeWwy=$A zhYbkv#C#DI?6Va6W6TL>CGP$B5UV@5xx{DtKf;ft7`Ay5CR20PITNp@XRY|0{0q8y z(JvG`+~r^x(j{?`w=+tZ{GrnYbZ5$aD{IUt__cjX+=prJZTk`LjNGx#oKS|Qtk^{Q z8hdR~0y&vr6Qo4Ur4D-)rYEK?ak4>#_nBdc=v7;vVIN>B*ID5yIO`Q2 z#8vpKiz-MaA~x^=d=O*lc@L3={n*izz6xuwbVllMetWN>x8uLtI)OQkzqBq0#~=)? zzz{-+Rf{f=7Lmw-(XfA#c%JOkk4XKFF^I#YKFgod5hTOje$+3d=B?%E8>G|g%(0#1 z?^a~tTgZnNe@Rr30|Q*ysG<7sHx;k|{(eY9m;bI_})S?l&= zmr}o6A;z0ic#F3X%_(2{a=3e3!(5+m@;xbb3pnrjwjDa15gfF37o4pMJF;mD>sEB< zDhbOxVQ$HBW-Ve%e~PUm%Rkua&;jQ8@D01ZV){ngZu*9C zEWTt_J0m(}@zU*#pAqMOxkzuotKy3{ zUZY(}_FXkaYfRm-ltQaQTK^hDYr{HvO;S!rR5>H5zeamlpQ7%J`{>{@>b8XAJ6)-( zlE*i;Q`f<7tU60Qn7(3Z8#NV8`E`O?kL&mPnzTCpbEi&{E^(#xZqln{#{-4r%`ocD zf04sd&utWw73m?Xu8<#~ZY{k_3B~3tTunJj_|Y>2-vbM9`jxK zf4`%RcnUeXkxgn$vDzh4>R;-yr1I_*p2vU%PHJS!utJ8 z+*ty3$00nAaBt%tf;$nt`hDV;q%W4G5CN%R;W^Sv^84;*p~}OB4kPGC)E_Nn*hQ4~ z{c!9}iti3DTp4A-##lU_{ABfSgb4DLWgH@rJhtd#l8~%-KA!i|rOehom+moX2G9B0 z_t!o9vlxMjE$1>%gdx{9^Oi;DtuW(Y6Xq7>vme1ze*TG7hrH}GlKsr9(Z)UNrXS%D znO_wYvYU{3FbuvWky{bfxwe4wYrM;fM7A{L{l)LG+z}UlHZpC{DyQ!q4@>l)`u^eN=}=|7^v7vG=}ux7u0Mf(bW!@-Pk zJE`BYk8w9eaNrQ52%fyNhyDl=yct2?oeoON#)@``8AB{Onl$I zg*Ji6vvi~{PCK=Kj{Z;NQfM0s7lqsGNPURDx7MGUgr%0REe#=(Nr6L4WUmu;jh>{_*->|Ksof@Bjb!=ldW0$A=$%{K>yQ{p|BE zzWnOzZ@&HR`yYP%_fHFc{^i$&ixw|gx@`H1mA|c8y=LvY^&2*B+Pr1!w(UE1?%KU) z@4o#94jwWyx3ILbwz0LdcW`uac5!uc_we-c_VM-e4+snj4hanlkBE$lj){$nPe@Ek zPJzKw5ozg26dHrY;R!?%nL?$}8B7*C13G&=lb@BHlbe?>C@2&biNwW{5~)mHTBcBz ztJE5;PG3=3Rb6AKtuxk}8XB9LTUy)NJ370%dwTo&2L^|RM@GlSCk{^@IXX3c?D&b9 zlc!FfnLT^%{DrxTmo8tqdhPm+o40P?xqI*agNKhEKY9A>`HPpYz%~h@Yp+~|+I#&5)Z$yW zZ{L9$eeeGL2T;2oJ%0S;$y2E9&tJTF`SR7f_Ji`>TR^F`{Y#ls=v{?l>LryUeMaqC zFkbFg-mi?T=~1QBcdF3Mttv`Klakd}U&b1)m9ZwOBfE=l(9BeAgCG204^Egx)K+gJ06x zQcqUc=S}F{%Les9Ree=4wcR!FhW1)aYeN;K$Dm~m>&uxF$};AZM9Q4WlW@=cU4ZJp z9PC5xscd1F_12VARW`Yk)z0#v8lQ@R`ta)BrbJ^?ExOH6N$IVC98@aCxJ*GmB9t?Z zWl7mHf0sWTEFE5z*@fKKSf^a6v?QOdwa7kFZz~-!xoL+w0`;v8iKbcux}&0!(x=ff zhf3A-F|m?`c(=Wxt1Ymq z-jvu_S%dD>Kn_YBW3WU+9}%i(<2g$DB&Upd^zQ=Hkb?qpD7OxNP+^{Qt?mHfTGxQ>!)TDPc@ z-YbASvLO$yjxog2GDrR{K(qZ12mOBkC#Ib-cY8L&XOFGJ^-gRTRChUR6b%t3sWG)h zQj2dF*U&l(tLWXikO#kl(Z|s<2UvR6;NJykw`~FCha5olZr`V^o5L@Ut&f{HyPVWG zzOzu(<*q4diZ+Ujh-P6ep{>wB>&UNRbY@pGx-+X7JsFkEUS=h$@9zS1Tekr1UOP~~ z#p6!@8vo;GmPB{WE~i$E?GsA7y;Vgmv4#RudP9C4p*63T)|O*nwDS$j4sH#zlU>c~ zVpg-e|1O|n^A=FO)#^#dYRB2>WlsH97RT3~SxJ|Vn-vuH`6==`;`P}r>4vOELVcEr z*1)T$H*t)N7Iq!8m08Pbqt~+A|1O|v;}+1k`rwtZg$GA2{p@bKwIoS-W*x0?(keH5 zI6%VdO;B>X(zTp+dP^P>$;%sxaMTHM)zcL3Poz zf0t)%zpR=*{l&75JKr7Dzg*~3d}n=p*2M!D<{4)i<#-5}bTlCwdl;6F8c!=sA43Td zqZm=@C{7F?A&6naq~erevgGe_V)D~}_uTwqb@j_1%%otsSI&d&QLO9MFw#X&4DMVQ z33)n}o_aEwm2v`}k#szbn{XV(i=W11#!X}Su~T?{%oH)}?*htpuK`l)LqOu{^-Sg( zb-TQ$> zkxo{SN{_rP?;QTf?p+S=a`>NP=v{w!*B{>XhyV9`<J4# z^c5T~4KEv%CRO!HQO2$ka$~!M-PTf+(cPHO?XS(&KLnDBHCL$gB#C^T$hWTC2&O z^?K@HB{ct6$~jZT#W^#AqT*9I`6VabmOmZj_8!m6ZNu;BY~fd`Y{|2=_WWt1n`olm zUop}Ur5kF6S9Uhy>P-f6MQ{ZUs`mMCoVbhw*2lOhn_{LoyQB6 zP1wB(YuHtTHR-I$hIgXbx$sE4k949pLebj+t7vM#n5rQMO$DQ0rlyY;DjAb}dB!o8 zEcXOUUVQv*0g9b#fZXyBD6@5cp|J|PUul_g&1gY5-(s0@s>?p__>gDOP+z#Br8T+Q zP>*iZ){r|Dkb@XngUBnVPjJduM`@+}X=-V~TLYB8JD355wfjr0W$1$%^W^IdX1KXd zbLQ-bb>@+A_rlJeFjZ}HQmxL2ZdO7L(n@-tP)8rihSp=)(E0;a!8<}$K)0yBC4V|7 zt=wMe%|jm69ZbC0asYj~{}AQuv^8sR)J@dT6{^)YB^i}Qbc+mf5Wk-PLw$k|twFG$ z^#`hoJ5DOkns{4)a>p7_YGDRc7OtT3V9;aJzPLMGdl8o=4q}d+ILK)mc9T?fgz1%y z$@MZLrlr_G=@9-g|L^1J83QaWYly1hjF2?^(YFPZzjiP;1DZoFz_2&qY3r`28^gPj zPMzA0?w>xuuN`!ksX8Jmq>Zq8krCS>G*H^1`F~dyH23Gep8Hp@`l))(07;iQ__hGm z_BEgka?tN{0;U~4_jTPR4`GJF=W~cqrwoUGr$2WQ%KEE!uvtqP>0Bm-*D*Sd&sV3myD*)IK|FG)Tl9 zNRVgrq?a?ha9UaiQBP?nSCZPO)r3}B4X%Y@z&5jLG0p6^22^ZV^Qw8prVEn`)(>9! z*{uHYGJoZb4JifZ_K|qUUAU~tkUaWmVi9F1t%Nv$mgD+yWtcwb*3e#(64^s3Pw%Fw z(z@wtL>E)@wt%|jD{c(^ux#|=x9b`o{cNpzwlY9?YkMN^B6M#184nuyL^uaOm6(N| zgy*GCqze$^=tB4yRtOu#7p066Mad&%ancC2IB}T%);#R|{-?vUpZ&Y#{PBD+>}q&g!sQrL?8PKZ z^c)Nqbpb($IFBTRpFAab(@5_iAHQqQ=X zG9UB>X&`H+I7Tp81e1>x;B`Wx`70Rv7uQ2iDF*{-EP zV!acHUF?C>#qUX}YwWEu57b44593s6Xx@>McpSy#P3>9ywiZrq zR})jv+dvf!m`JkWI=pH`kJk)o32)9H4puusv9mppIr}~-bBVdFbVFV&_oAOx2IU+r zjh2i`Vd{ZmY;})_THh(;G_~dO+M6@8dK%by1NF3`kvg(`Tu)GrmlIVZ3X*#GO#$-P zIY15)Cp#c_^nI#yioUIKNx!7=pq)|sXHS)f7mt@EmJiC1mAxe-V`nj|sjZOH(UhIp zSI^BIu49YF3{1u0av%I^*(kVA>19Vm71d0OrmeMjS*c3I~}oz?m9 zk7+|h6Y4m{uo9u~E5p}z$>@!3#q5s80$!gnCwI6iQ#h{W$R}m=vLj-8`Iv~V8hum# z+rhyOl-c_{Q#nN4(K#ZnRJf4OR(WQgs0`ejkol8N9k6PK}21#|9F`9qn*!oe5>s)Zp8sdP;Ah ziaC_6;Ei!g1&5iX@=3Z_Il&Mq$KMoCx&v~s*a_7Z+W@tt=gUfqu!ly=#9J-asH;5= zqzmJo^b<#;*aO1}qNXmGrm_iDS5}8_6;)BXbD?=R7n*a^Rk`Ej^5O|{nR1*gFCTkT z{&Xm_vH@Cik5@HjA&(l&;_r4^BCd@%V9w5X{Bn$g(Naw(TqlDZit5Qt`Ow^(SH&Qn6%S!_(l-W_ZC?sx z<~u>TnH8wo@A|4?uiyQiePNdlTST6iwLlD*$tEio{u02RB4&J9lf|CrfqI7uHu+2Pd4gXRM;1z$n8wk@mq1#yw~%8 zXy#8hF&e2xb~CAt(~7UnY{S;(v}0@Y-WX86Whp4%vke%w?tk2|!F+CVquKb}2Cw$p zTNA3z?Ip^l?K2C;0*bN+;^e$;ggT=Gr)Rd2s;Dg#1G$-A3!RH?#5J<(F%4W3%9PcB zGG#ZuDL}P(DbR1-3>w$&xHhtK$K<({W}Ua!`B&fEnk+xJpCmZu#LJurF31>8kTCnx zN@+dVa#A-zgYP8kupLxArk!4aYGYQVw`Ei#T6r~y*335sXf`YXwQJVi>s`8T`phru zyKgSDF+5!xRC;4air~CCfji^QW*iO8rcNXl5JwPV+z_e+J&2Pb2Z*I<{p2!4A5{U} zp;-Lqx(WluK*3vTU8;#{=D&}O}< z#F@wp>~ta@c?6z=m`u-yPhbQoNS^C{KWt)>Rco#^-Mf1`4o(qcruM0e*%>e zdji9aIgaN>A0zUjrpcL+)6jL*Q;e+ex8Q33=bsJDeEOy7@i)s=;Md(ku-1ZWT7L*RM<79K59Hvp zR7%h+?Ts0p_~iYj^PhgJ2H&j|fh9XL!DcHuIOK|d;q06C#3v-_K~QAOz3`ZbJF)R$ zw-OUWZo-m+Zl|9X{>r??ecPbB8Jf2Ng4D)%q0Zw*xFDt*Un=UkUQN)d%q>`pi zLTPgcuDqoKqibzP8(N#uwQs~bhd=Uqm&3ap{^!{FU(p{VuRkBGHh@A`Gaz-UJ}oxd4cv9EzM@ zzx#Iva@T;zr5^~g_yEo0ae$Zn^Dul&#Ldr$ZKk+3tL;s zl8zQaSywYo+f#?F=&8U}c587}oo@mtdF^1i0f?Lq0jZM{D0L2atZ<9Ht@KL2tn{bO zmW5{?mn8@%i__&JMMTYjfLYy>n_1t<%V}<77PPleio2VLie3|5*IxlGYpd`TJqki) z_nYvigV^!)yMHI3aPogr?ha!5+n_ZBhh+VglV4VihJhK!=#N~U~3OV+SyuuEe|2wAKsB^_!YVf9=YYyhl zG{@(T)*(tdbr`KlL8_~i&|AudoK9JOPM;`8I9QM+AI{;aN3uBD0X|3H_a^+egUtcx z-M>B1T6@2!v<-hy>yUJ-!3A}t)r)klGnhHuk;oq~!6mJgD6K(7sME-)&GKS)r%0I9 zmtRmYn4K>j=H;qKI9b|34qxB*Cj99jwLSopR`x(=<@KW4I_y!sUBaCfXl<;^3pYCu zMn2pfpV`v{lQbC8H5EEsZJCnXES57n^Cg+Rd~yB&r${oyEK(0M^ReJM!YK);o zM{E+5(>wTO+#a?(x1TN*4U(m*A&N*lKo#ox-UJ}~PY1c>exNqD1yyDqpx!*_ahrA2 zjee(u3zNP{(?`Ov{X;Rl#;zo>z9m&9uSeAgYY2^571TDiHlv%W&h8_qiUzPM!I+U941KLA2z;M9rWz!+Q``wnISH_(qW>5LWj2{ohbc{!H3z3Fz<4s3R zV`}m`k=5d^wCeJ%)ar^i0?4<%cGw71`}ctAJr*yUcG=(Q+v{>+%F21_oOQ^+c?WpY zX?Kc#Dj-Wb5-l$1gDJ8)&^k^FsfON2Gm=fL27EoY8B>?tf~qTQMb^n%(`uD%ZvrTV z9ApPJ0PXIbpl zLzj1XwcOYrS8>r2BR%QN%0CjAn>i9IW)Gyw={=Zoau-31?|@E7YhzTQTG`cUEnGut zb9ODPSx}eKBzhx&V$*`(>rv`9ZhFwYX7icjYuArl+F;dsZ@YiRt-XoTIZIsLNjDbv zXmB=jJYGm0Mo3=QAYl9O3RDmIb?w3LbqDay3|&eGbVFoEZbf3d;Ee#vjSE1<`V}u) zep_{MV#)Hcx#e5i?yj@1e6lr2c5{DH-kc4VbJ~+ZI~K+xO(y2z##0MXqbO0@Fs?Xt zh*$y}q{xy7=<=j~R%t>Xr!20IuZZo8`4m zYn^0Iwg>0kGE2<3+&AJ? z;}4%7o&5B}u3KOHQ1jxaHKkz1{(P{-G2_J{KguJgaO@rEUdS6EDT!Aj5wVx!km$SSuYM{6 zzpNF2ReL#Lmn{WYxnp0r`=>tk3r%Z8!uu-yjp!Nu_#btrKmJq!zFANR7Omle4Lc}czZnMD+rfdm zYaH!1!?+UN5Jp}xh*DM!q-(1C(<*EF5w(UsM3bR2y?NfebNC~xcRBno z%pvc;#)Y^0ALAl4>1II;=3*Y4KAVl>&t%dIrr4a~!}RRZ*W+S@Py~$&iGC0xuN-(i zE|A(9Xj~Zj5p{;1v?fD)`uzF5KNLV?Q{cK26uDUfv8Vg1V&9O5;-JKvMUm)B1&OrT zJY?of7P;Ukk1aXO;+Kum^3=m*p>B{+TseT1*Yu&4hCYc&oVO>-l2QYDa(rz|8KJIezWk@dN>F6K1A6yw4$2&yLAkTvGqqdP zJ+(LNx;hYdQ5DXZElcE|kfs+N5fP=Mc}#V0W@c48E8o~iD{eHB%35j&`Zhhjrd5bK)f%kK_F7At|sZaa|KLN)JpPC(`8`%LQ^d0*$5d{gg_xug%Jp4G;2XUY-z z<5Ik&uaKs0%jQ%!aC7PnOkq<6P1dTRsN0p~>JBNXx}%tEXcbXvo97D&BtGXtOU$5f` zn#);|wo-<&LrSmeETU9(7E%pu0$Od$eEHL%*k(KQ-u)0zL2D2?JMWiOj-ihXZt=J3 zd=M8+!T8hl@wD-()a>4Jv;+!SWe_r|bx=*GlAqTs<%-(G8Ks>@6hG4w1%kb}f(8*~Q4 zL7;`^|5a8VFY4@q?lrnZU2XGDJl7qXdZaCm+-FSAYOY8tt}e%_R5EgvRLnFA3;E4? z0zn%;SJKJJRdunmE4$eI>JE0Mp>@9e=}=<16*~Xm0MJ46{~Am8XC__#h3Ac$S%4=f@#U1oQMHjW8s*9Fa-9gVW zw9S`49L!b%srgpu?1BBD0-FEVnY%o0w(+^y?G!XO;v0HmG&FIrH=5Yenvhv-go~6_ zNM*4WUy)Z%t>cwuG%{t`t(20Yc0x&MC$6NblTcLMK`Jn`%@QT6sk~N?y~Bl^ELQ3y?q#V#uNN;AWuRw+GbjGk@NE!0t|;rPHNJ7pGIF ze7r}dg5ujoBXHIIv0O!GQejaGqBN@kt!35`t4Rii5mU`;NUIVw!z-mN$yK^mxVE+> zO*w!5a5%IQDE4iF>JN5-`rQW~cJ8ydI%a8k=CqT|48hx9Y__al~93hrq>{vxOFKF`KCmZq#@p#HqIANvgJD*|F#5;GTVFXDs(jp0{!AKkpRWc*ZkXeLA?|!e#soJ#adCU5>S4P*ZnmW5_Q~%XnmW?+K_~@@%#gtrj zNY6d%LC!c4oIyPrn}eT#7obN_;KYaXs1Pu{{N<=JRXRiKApu{0KuFJDeej9?C3<8qAeO3<_o8110kL0?O8Z2P#)Dc+$Rj z!I|lQe>-?(>58TYYxn4%Y;!7kye}~Ku5}#ysw;wY!4HR>jijcXN?^iHz&T0N$jtaD zTvqH6QcmT1kP|FU&~Yv|@K`o4U^+i@zJTh5pWf{I;^WbipMKnY|N8}cuw+9CSa&cN>~vzi zwDcuCatTA<^^1ky3`vT=9tn@R5)a)%oPr9!n1%_RLt{fO;BY|~i1@(s6hgo`I??|e zo8)(vNA^9NMVT-6TR;C`Wc-tVv|jw|Gd=imVF_5iF&Avy&jS1HNWjh={le29_QXFV z?m8g?L+cMIzBlkNpX)@J_jL-~>l!`P^BNoBah01k zUq<>qey{cD$M07>{NhU~Sg`YMb&yu;Br|>zsGnm4HM!#jsRa(b7;|HvT|>XSUEPv%2?Qs8X^3IaS|3c^Ak<;Eu5&Vr?1 z%M%+5W5EhwBuiA7UsGRcutrF0UeksnU3ES*TIE1O7cQcNUvDTb1J z6g?^P1Ms17!G|394jX{LZ4VH7+5n-i$17oQ$Rj~y{O#Pt^s9U%agIY~%`#a0lVpDW zaeTgT3L`2$f|N=RBNU|*aIIoIrAj%LTwgwx)T$av?oxHbx>T+3`2n(icUTCr?V&m? zmp#xrh&2#@J>xjYKPW@m><78WF1%tav=wy({5-Vh82)_xB-c8;1h9h z?A@ZM)N6ty{9F!-Im@H)PqH!u)0Aw{5ki4<0wXCKOD`)QP1R{eV71!elvZ7DYP+r# z(XOvgYtz@Hw^hsskPYRK4LRi5tp`PpyMe^n5=h-#fz&(TX-Q!8z2b2AbzvOtVm^X? zHj9vXlEciOre}&LNdnn8wpcleEK?67bh;t9v7!&&R?(K$T3MgoT3L;3t<gJISZ!O|v-SNlLDK94{&# zLYHd$({&Zyh`OqFL`$_Py`{Pu)m*JbH&?4LE%O58Ksn?S`-6#yT~&u}*<&tdkKMYbAuHc>!{v z9CD!?gw|_83A8_{3|fOw*+J*-IeR_Rc?93n`^8?>hr-WmVzDQb2-;{dHmg@a5w-H! zr6zW^riNZvsV7To)I?RCl2Bum6B_Cz#0H~?)L2(UZmOLRe>xOdt%dd>-qyCD(!uSa!PW1E(I;%q z6dX5G7l#wW**pm+b!+}q0fMUB17J)?{F)h6$tbFHC~M;l^N2da`Nt*SJB zoeW!~6Ol_50+vRS!>=sl7Z`H6QezfJZQ^li8@L%J6PMj!zR~HuWDs%Gk z4ZNIUBRi|y#K@{?VDRfr%*=**7O%lLA3*-PFF-z&gXGX^pg6Dv=nn1$HRk3|8f@%t zwmG|8?DO%M=?e}SZi|Gs)yI=-t6-TL4N@p8!^?$IsyauUQNbyKP9Z1|87TsVi6p2r z5%WzZN{*?Xn%!WW4}Ut899#v;_iqLj2X=!xvqKMBtgWv1IN6;a^L02f6d2ss6#;8( ziX~OnCG*NE(+WiztTb0iR&nGkJzbhrLy{EM;fm$;XtCae5}WF=g$+i$ps{W~00ESP z0LnqO|2LrCy9re9+xfEL;67*{%AqU6&gN&1c-fDQ1_X8Vg(cUu#}L(xNgPRCsvy4# zUBcB7m2@>jODN}6p%nr{TA8#CUZyp|${LJ_lEyluxUqIVfC9*&aNj~ux@RR+f3N}6 z?%DRVb>A*%AIk0vQ?5IY&A8hRO!@^j4TmIE^hV+39SIqQO|ZPodSo%Z7GFxJrm0bt z9DQmET4+6}d^iFt>W^b)btPvr+tY-E7OWK6L{Y#RGSmsCTwScGxFX7= zs*E(%RYujeR>oG$4SJ=fFj7@b#0J#^I}lHVcDJD^-E`uZ~bNB!k!I1SIiGJT(oo5opTN;IpdX>domEk zIvznGPQ_;+kHWK34rB5Y#>m2$5wBA7SblL%J=(xJN~@vyTAl!Vi%jMy18JMsjP z8-9$Q88XG;2OrJO4m={r@jp_W>o=*$^PSYs3m{+nIjCCp<%6ytKbt=N)knS87yjGy zX!Tmn@LNy44JKpig>%s8k%aO?Vxdd$V`BXyWSrj>JmOu?TL!}3u zW-$Uz@tFQ6b6CDJ1#F)ak__(?Wt{l{)XP43+4#dp(}zEOzwg>tpVmKK_^amS>J7!O zwjIa@2OOBMY`h6i+=7w!{i2fZgv7_*j7pBYmXI2D6`mGy8Ho(KghK~hBw_sK=vdzi zY`o6}9>M#34$<>mA<5%h@w@;v-@kvf|C4|8P5GxTMMZ_x87fB$Fkfj&=@gT0=lhIl?khPpq(gt;=XMLON* zMLFKjj-C%aT_3(zfB2*K)Hgr!1AyyZZ{WS(0|Xv&1)*lnAkxAS z#9G>e1S>m`4E^ANLmMyCY^(v&)(T*3Edk!n0+8*@0S)?r{MP{Ls!srQ_YZ(#y8>`r zHUqx*evlPt2lx@5uQ>6+PZ;pndt@x^7M_N>hUOA3r{z-T;6ldv6bbuWQYrUrf+}-1 zPR~CRTa$e{#*}k9x;5uiRB!I7sL|Y`(T8)#V<-QTcMgAK^uHzt8q^q*n2cI>?V`E&vMY^s=dCPkigI#HE#DnXxjGOjj%Cbp^I zL`-+ViRjV7iMX+Xf%vh)o`liA1kfP|I^@8%S`Km?H$m$ldqDxT79$9B2YKOvPqX5q z?{iXNHyJqeRWhA)5yxYkN9A#55hDI+m@MaHQhEMNLPf!exVpmQv8}@6F#|=1<3@@G z6Gn=<5=TU>NyC2$U_cHG$bnxH)-9(vb4k z{O-Vn9C((?fWUquD012ZogHKeMBXl-D9Ha=L1g5E+=S#?nQ6$YEFy7^!eX4oXK_!X z3bRilN(9GK%7xR3Ridf*#^R%KeUjnCK}mPgU`b2LfYb;ZkQ(5B4PgHE33%SzvR zZjM0avj`!of6oaUWbG?SdIfJ5pN9 znqa+UwW+;|N<^7fNH3PDv2R z!vZXIFq6vdVsY|Xs5v4Np-5`L%FC)ynsOboR;599s1>L#wH(u>l3=@4Vr+N$U-7#` z2IL?(v;?aCUkgetw?TCnQ0={=^%JeT%Wa*n&t+Xm=vj4K+)+6!eYgls>B=Q>o4L$< zBa<(xq6%bMqC`=SD_50at2HuAi&lc|)QIq%8X=)eT|nqk{S_bs%Hg#`!NDa^z4_}i z_qIZ3FzkJ$bFh3^>E?K&+Q)OQCOGh9MQrrAGC93hl1^zUz;lh+wERjASFC2{%F3uB zWeK@dEhg%gTu-vMAO1?UaEiUC`$%oZXHnUy6wI9u2>KOrT3mdAc2rV8X=^R5FmsWdhfmWE+8VPAYd0PShpP% z6a-NOdwb8?`#Ull>Zn1YKT6x1hS5-fx>^h zx8@!=rla!tgrUaBX>;xVcD%vEHb-1nlQX@&)lJu+N*ryIx75PJIenHh(;RXR2D;y+G;3KL*83`$Bz?7_r7n@Juq<5 zOyyY@R`bRsg7vwx6xzvVrl6{x>swIm9-dz2l@MR#mmZNHk}J=NDe}!qE%VCAtMEv# ztQ2K5Rf*HvD?O7tD!h`;mH!w2YDhS^81htCq3?*<0JVp=er?v+`SzU7?w8lic0K5} zJaYA_omIVo;4MyG zaU@rsb9T#XWlNKrdBIU9#4$nj(q!4Oz)X+o@LWMvLZNF#Rx!Jxw3J!VP|B=4U*b}H zp~SV|zkyh!;hzp=5t@mq*}wW%^S-sC7Z0xOyQ8`C!885MosZ4cTJPAH)!lF)6WTV7O5?c#TJ5Dmdewge z@km3K@_Z;&UIO*Imw!IHYx%$pgwxzNf_dB!83|?-Y?#zBQ%-b9+yUG&&(vXmt;A#*JlwsIL4_2T{LoK^ZZ9ow#~WLud=G+mDcW)eMUzsdo9dzpW+FLk4TL0`_6p7yBsgc zZGk_p+cSiH(?7!bMpz8_Mtr=(^~^-V^^zpJ8^@FIH_s*EySkI@F5XP}FCYnNC_))T z-R8eOwy&Apb8p4e>-}36bPONZ&^UBtf5m{lPR>hHtAswRW9SQermUCD6ZbGZxX;|A z%x50{)Th33(v#2-;^Vk*`-ka~_y+}1HV>*1P0_dqSEF$E@Bb%|wsAUCY@YF}dF6D} zfl#{9w`5xT+s#W(jPBc7{#IQzdqmeD;f*Oa^bOWgHcX(4hMinFLv#USkn2HxCH8Xa zm-#rn2=XQLM)~7=5(90YWd`A%6$W9SRt8%?J&9=hPatpI6sTLF^!noLN!<@;Dz?8} zI;-K+=9Oh1m3L-+R9B1tsAm}R(ac)*5l0k#L~{`1WF}*j$)S$8xsis&0^*QVWIq@v z#t(!^Y+l89;a(~R56N`n@l1x%crNq5xPC?n(|UOdrvAZHg(8?cEgjY@jDekNgW%8(DQF)M zfT21Y%(batqwferV|yT*+5*EI3mi);;8|OMJJt+5ai-v7V+{VbM!$mbhQGq?41UJg z>HSQy*ZYx5_%B-9CS&SuOvV)UPEkyQ1=FHo{emFay~-O@H}T-;E+**jBZJu?0$|l` z!TzWY4yY59JT&z$JGBlsy>ggVabD{ET!!ZmbQV=tF4~8dQ$^PiikI zVhZmoVp2cPP>h1bQ{}MnFHhLDgbVvuJA>LrN6^`d2jiXAV7bQvY?V!cxZenz4jKSW zMGu&VbiqYc8@Q@kAULcEBDJI7sdfZ>)HT2l6##<}p=c0rV!&hL6p$R83!b_wA;4q{ zgkbkWDB&oCP>djujRhH>_`}ni_E{9f{=kpsz2_#mzvE(-Y`m}!}Lno5Utkt zHMP-ikaEUWA4H>E zEQ(|ZVRUPdbBW(&V%ldff0uD_IDb?SFCO8hc@1-NeO|MQ{RWwp0R!|p`77GVpqJFM z!7nM7Ltawuggm7^47o#p5^|04IP~g&0ABxBgP+z42shjeF(`|Pv(c;_DL@rIY;J>;70JLpmrFu<&kzhu+}_d7R*^wZkI`lwxDJ+yma z_vnwpt}`BnUt&FsxWIZaA>h4X3V0uw3jvxdAkttn#G@?cpPp0#Q6J(cmJrDz{0Qbz zKl^#PjCsoi@5E7(VL_75Yqu=_0ZyU(C95L1pHUauM?V$*f_6UQ1+6>s0sTSb4d#Q$ z%dEen+S&J`&$>*^zZ#@SL(tI`5Tm~dlFat}+et~b(}P5k1;jG!e}ubIKg-1~V={li zThDOGuqZ(`$j=CPwG!-iSym)o6P&sm)ZAX+FkC(p61+* zZFZd)kfL?)K^j7iEQdIh#iW{`vq3CWAKW6R^hurGgQk5YHh= ztn8IAEwIlmFSM6a9`Te_7yXENCgwila_lYU-MGu_J8^BCJMpJnZznWxZzmjgn;7sx z8e|9NLAb_pNYvd3=|;OC(+th#V6`E`-sDFb+2&IclQbH`V-AOT@?Ql6N}l^f`9AST z3i?}^9d?^n9Cg#RI`*1NW5QK-d*WsG&7?NhTS?8_?xf>x-N`ll?xZUItqB1c(%_3U zM5-@?WbF;;3?9F6Y2T&T~IM4WrkKH?i8LB3b~qk_*#lOoSbvf@sQ zijqzWt5O^IC)4Zs=hABg*VD`0ucsG_uBYdVZ=~h@2jGj=!GHfeh&{X%GXC-Zy4%n! z2>LEcGqo=zSl!WLd(*)pvd!~6HtFwl0sBURm-td-fNy(nc*tr0xTr>-wD@|@yyO~> zvb0K3eMXt6HLFC_m6h+&m6;>CmYL~!JtM>Odiq3w-})&Kux~EJt1g8s^R@9tT^9mnG*s2XdQx;=R%Uo639jS-bI?5P!D70mm0%; z@9NACzdB)~`Lw~oxVxH0xKzwxw&V!iPo#OusuTP|N~1%g^TT5jGDA{QQ{}mti2-Ff zasJ11V*)yIqXVwyMg?@{L1y^!L1JG&|6D z+UD?sCW28{E!DoGoXu=1;JepmdC1CAWWo6fK~b48Q3=VBNvUyRS(#BGB{|{2b$Ow| zXY)eP*Oi6@U(E>#?#vDe?#h}72tex)w&yQML*6@I4efhttp8P`xn;aTZ|CdNrhA{a zVGrIpZEtwFfo#`O&14)eaYJdO$fqDv8l0IL7?qqDo)8-spBfRJkr^CWm=hRTljk3K zCeJVOO0Ivz)trFv&g_8juB?fGK%^mJ_g|24a1j)$u7C=)wcqQGY#eRY+4`!@Wc$6bbEMH-Sw8>4zZ#-YHz@1CLexK42Gy#oznoB8H`1!Lq5qQchQBYH?Z4V# ztKWLs(f0TWXXo-`F8sU-zISSgL>^lpiwMgJiVMt&N|vQ3r+cR5WQkJCbA+jlxq{S7 zIqu1w+2X{mERTe)%!vRwT8EgOvmxie0w_Iz_TCS!_|SZ418-<*Gn(3N&lIsz1+}$s*)+?H?ts42$QN$0c#fGE!Nk#p(3Y`V8l? zjtqLq^>kML^>lXL^|XnAV5A{w+e|1#&qmGe`CpoM&L6(CXKwF3)tR>+9a(wtuF>x1 zn^s!Yo%WXb7oA8c9dve7E5|+Pv_R_9BncEZ`iAiuLZaBmW8>)c>50_3qGYGKx)f4< z8=^azQrVqMEuR<&MHtFL?O)Vn?VW?t<%x~%oF;m-Q|mPd+j z;mtCxI}&2A(ip*)*gT&Le2KWjL*~{lle5nThdQIFG;&K?v_nf_ETN?~&c5|*tlinW zv4oR%;)o~i#7_i-BMsSGra|SF89$oWO@H0FLh0VKwUe*D-22zrz9XwnJU7@`{>(xx z`;o0t!hHwZ&^uI$tefQ`zQGf?UH6c%x@11Iu3$f>&KNndGcDMzvnbTIvo_SG>uf0Q z+PzSl3lGC>J03<%1jMXTg2GM6|F2ORJGXGs!^d+JuD)73wRLdM!s7#K>&ss1DrfbX z9F2c&WfIzBXCr&&L=rz`(7BJf9Oh#&pZZ89a(oypAv}!rwtblHgL_mYvwl?PYxVf7 zuho+~GON42e%9T+{u2Qy%O^p_nn~YJ&sV&AeJ1AWtG^UmM%K=(8`-_A?fAOCipF=Z${0VqJ$772 zC3xIK+h-hW>^^R1HE_$HQyFYDg3s6e%iM^Yhu2u?Fjmz zdrw!P4;;4QKa@jko#9x3~W8LA3ZLb2R%JL^AmjNjCbNKr#H3 zK{fc4Pt*Tc=B)Rjp04|$aYF7NpNzS1b_%BH+7wLnlj#ciuy|SuY+4uv`_{-oW1Bbf z{(LYw%mPbIGT7=8!NJfLs3uronOOqY!W`T!O~K3B82qq?5R49xh_caz1Y4b7>3Hp* zxprDVOYOCOR7}XF=E<0o7basW?a%*kE)ZSETV#R2n}5DW)1|U|R!i z(oIBD%Op%imm((n@gz(l%vFqlRnr1s`&}YiMwEqXXz|vjLNxR$#ry4D9w9 zgX4Y!pd8c#hKdfj9MT4!D#{)XA3@oJ2FTRaL9THa!j7my^pQi5gbpH3nUKm07)IMxSxgW?9Reu&;KMct zvCtL-GKU}BFzQ!!Jo7Un-Q}Zm9(SBt${!8^teTNCF!KT^qdHATR$1N$}>Ttwh#ieSN)sCgrkm9n4LNVJL!Qx(;U2bwjlC$ z{J{&RedWZkKCx0=KhSe|W3&?CdrGzFozn@Ax1<)yTZhY@BM$eyUOM)AJt6mb-9`g_ zUDST>3(kGgi2xqb;D$7KsV;yZ&6N;=vY04~y%1%47{VQOA;{Sbd|hoo;^FwiJ%IMb zEt2)oC7C*d-lwd?I~DelERD?epoRy^vjSe&KtT z@!Yp{0{+#&+dC6{4lRJtqbngAE3ofr@z4dUH1AQ*kFjzbwl zs_r^;9>q4uwAl9}(?w))P$UIX%0K(av{9Q%;IW?!)jhw};_H{6}FE0m8MDL9%lOgzlRMiE1c= z&{+%FhFj2F&Ymy1Sk=)Sd#%@*WaAepEbOB=f#a<(FJ@<8ApfE)LelOTFFPYn4`>qR z2cO_qgw?w>L>%L`MOJffMwRpajw;M;g3#%s@X{=0Y;+#bjx( zhCKaEKMPEDeJHd#@TL&2(Vy?A|0IiHc{|C?u`}9(c|O#a-x?SqX_CeGHh3im9`nc! ztrC?)lnIYTl?YD96bY`zsT^-IRxHDU#r2La1a?iTJ2;VB-1bL}q5v(z|i7(t9z{KKG+10>o=3gCFXO#qLJFS9Kv2Xe@&=%{3pY z^fnIFnrwT~V7>ePF}tH(Wn}Y?d=~Lk29HsnDUkj}wnHMdBDJMIDvqptU#qir z;Dph}o+gW(x9V+=T&{F7Yb|0BPULXuRcRt#agtP$8yhG~j|!J3hQ)`)hGaxVpf5!Y zmDj`v$F%tnEXdUF6r=j_z+30N2`B0*|_-nQL^0y6|t6rWm zSo7$N>5l6sY&6atb2M!(bGEN7;5e6N3*GY4JU!Br{A7voq4MaM*s!q3)Tp5F{8<06 zs(9b9<^-Rxs|m8OJMps6yK%lD_hKgEUk$;Vra|(qS*ZIz7s^!@eyUYnGTd}z>5De~ zW%te+Z@!QJywl*u(29!xvG?H!d>KNqIYFC%oa(7{9iI-PEp}(IrFDyu$6Bo|U%824-7RR_` z*2S{2+7b6+UDEHxaFg#xPXu@&4bjMZ=j@mU)9gUmMH`*$4hPG`RvIzlG@BmS#N&FO5W9TEkHW|$69az8cPFD~qhzzv&-%5K-*haW)N^NvLidwRlP^3xIJfzp z_PXkBQ|0_?Sk08H1hc4%6g&BOCPmuLWr@y;+_)`L5$lZHlXg1N$LUnEAF(+vz`nUU z5P#}SpzZ0Kf%ucp<@U9I27;F;LdHr(s9ZDYK@@elQn^>%S4Qf%#tazl4SHpvCVgq6E9@tlxb#!4=zVNka-V=e?DKNO_XUt~eGxRPz9hPN ze-6X6zk+4*@+8an#WJ08~NorkUUsIQ&HXb8b{ zG|s{3eTJjK`ywa3v0Ae3*cr0U_%(|5_yA(?k31_=#B`lR9f%Gk%<-F3F=a!u6>>hW zoSOV)$Nb1IDk}rOYHjxVX0(U@&0K}`4W~i*X0PS&overd&M?G&=Neo7a5pvm;cag8 zBhXU+XQZ|6&m^q&uWX#=uTq;MfPT>YYQby#x{kQm6#?)V)j48Q46_fRI zu3`eLogNOm<_93pEdk99ZlJ%N0j7JM!1|y)*s0=xq;3h$N6`$17V1A}8-RzdF38ZA zfCn2KfoMZiR5wybhm)&8iOFH8F;#_A6LPju5mVQ$h$+4?36u6r2@?kk6+>aeG+)>~ zR|E%_azJA>4RlZjVYJm2EKmkvgE9!BvJsH?>jV9u4scX7L2w9}^uy{PQ#%Yn>Z%Z> zp#n)q4npS91E`d_A1XB`qW-i3rsxU=lX@S6iTW^IAqW;Ldc)c&eAqgR1$*a{;m{I$ zIJyE0dTY$UWW5nsZPWwX&DucR@=yMt4s?`1xa?2`-oM$y0g&$A4}t&Y4tpVP?`}v{ z-h~b&nuy|a==wJ?nD8D9CIDtBc))x`4lJESgLPAhux%y|l>agZ)%iwnbdfITEzty% zWg1|){4n5F90L252f=aGexR;a2F9Abz*)N+1Z#JJWZe#st=|sv4O=06!xo6&I1xbE zG#MQBP64XgY~W}w2ENG}5Ms9h-(erP(ow|1(+0ZK5GcWBK#H{jVk!>obMatTVh^@e zM6juM09=zJV9%0%SzmVgVSS7I-Kv-T#p*SA+^b?WPS3FGHXPnS)b|H8`Z&03pW??1~A1uXF(0dPlHnBK^X( zIsL?5CjYR$L-}U)g7V4wHRV0_EoIo|9pxqdJ>|LG*dGBZS_en84h+@Vz|&lW#*5Z~ zht*aP+be^Bss^qcZD2`^z&X$yC{b7-CEEfq+aBzTh+tRg_zPd>^wXw^{1ewk`GLDi z{f50y`(oWs8^;dOMr_{FUg6)-dhJJPkL}+({|TTW4WvC&fTc1Ughv;FC(5L}EVhCt zUKzw>HQ=$efg>^krk@4SB5*)X#DilN5r~D3zw9fVe%jShe%LlqzuUCazT-Nbzv3Rz zKVo0e-`Nb)2k~#|&+XsQ9}-3xcZj3RKLIq9i8&z+oP)DLtg#5BdMFPv+kz@rd%@H3 zFt{^N^@wi-Y#%h16^a8&ygiUI9DX?zIQ=A)Q-0Xj(Z1uGoxj<3(7)PTXMDyzV~*Jj zFo*EN%ol{W%tyqxtXmH6*w-9K{|GoE4dmTZz-|956fgcy7878y8T_$(LFS+ep3d4J z;2Htf(-K&68*q*x{G_Bh{vhR%zd4lAz7USlKii*Veza?6eZY6I-`hTAkKkXs^by{$ zA32P;bUVJ~TqV7Az3B93fR3^t>aHmu*f$G&kiQ7jS_#3%n@}FK8v+T3Kt|O9iHi{k zM3&#V{A-wu8$msxi?88 zZdaV%@XnFn@Xk`+@c#rbkOumWDInTA3uO=sAOv+%B8)adgyk*>wNnAP)6pL?rok79 zz~Y0@$9B{$ggD|7OCDmRIS=3gQY z3EQYc!qc?Z?kAo93^0)f);82n+C3BG2T=}kbUDNru1DRR9T081|3|o^#-|{<{(BkM z{Eerl&7gZAv7Z-7dCp0qKV@dIAJL0k@6)PzcPNd5ZgQLO2Kl;s7x{(g0(DT-N*fe6 z(FerG83W>4=AQu;%A+`2ltB8A|3_Vnm?O&|L4PeIns5D?fK&byOH><;py~_-xtPB6 z6XSZle2I@lp_IG4IA*tN8s{22k9U<>CcI3q6Rd>W?`y(BQM9Jo?Gg5%(d31nsY{0>C)+2&Ux-z#C;*lb9*7n z=DqOAkayBA-|eD*F7L5_7QfdoUGUs5 zRruUDS@=TsC;rtSMz4vGtW*3A7^0I=$#c2WYUWtAgvRD~XB(0H$OHT)dNxOnVeV)lf ze4Yh{`1AyX`1Jb!32;Rkq^l=G)aEIWzGns$9Gv~ROm*&?8jS^g$8{F}U2nLds|vfX zqlBo{oJTXO%W$zPPZm%L;yhVdQGUGC@K8}gNUT@1JXIDRkcXxUE9HUyO+f+vSAzWm zo(B2*KMVBt?+FO-Ltnu62Y`z-$W~5*7_`@$xf}J~_s{%Ld0_VI`on+qG-=MieZpY< zpyGhC+&IRSpYZl+Gh(Cws6`<7IB~F+rlJ$SAMS@MKwFXik7%aJk$kxG~5( z_+qeE$W!#^vp{K3Pk>b3>;EUfZOtU`TY-EpdOmY@PW@H7clzit<(V%}9-R5Owv232TEMW)&EYzwWr&#ZDN=4^VxW6Se1vB}OoGfOD$Cz9vP3S9 zI3DC4aW2R`@==f|;#r_LtS7)DwAcSn03Uh(0Hh%iy=Uj|nDVV+=hTtoJEy*AQJ#9Q zU1jdoGrDWeo;2Tmq7JW7Sw%7~D5cwE6mm!jxk5%{mX~XAy1&prHB{oA6esmaNcR)Q z7X|X-YvtVdwjkGpdvdq9XMy~fo&Z5qum7I_A-WGi%M>96Js(Bels?sLQ5tO8Jo(xA z?UTCCADnZcO?%a;(`LJlHQK0`)HxVtRnxFZ`l+llTq`k{K)yI5jXtEdCz@mS%^TRn}?oc;Qbq1NnZ8tboW|s7j8`e zk5wJvPOD7xAXnsgIaX9ki4~`$gvx7D`-*<4Lvg>9RM7AJC%|`+BE%#AU$9i+Q~g5B z!;YDl+c#%ouH0U!&~j_{^!gj>OG>*8Hf3M2+@Ej(uNmG!G7e~S#(K4I2*T3>vg;{N zIV5p&Ufn1gv#I}LOF z%rwje)In&uKNHi?vs|IFXUEk1XNTvfKGj_v{m68?`~mKu_dTMf@DA00bDM3(xW&g( zx+QprZa<>k%`g(~W&+vjW;WI0W`(oa&1PrQ?k;Du?w2%^8)FR9D`U(*0g1CQ&q^m_ zZk(8cIoCD?bL!eOOx@F2n6j6vCgr}|H8c6;;l+{tI%@;_OtyQzu-eamZm;grOV)Po zWg0m3@Jxt39v1i>Uu$enD9*Ac9&g^0WoO!3W^de!0?p@_5IqFLo>79qlaCI6VkCPq z=2p#Q%(-SIOw)NKObwa^DIS@tkTtSaDRE@?oQOASO9S5Mu9Lhm+0J`orOX`0AEFF9 zYB&tjwd{snb#cS)23EsTWAisbrY3Kq&5hoqTIi1ySm}-&v(|arVy*q|8dm$=ORUz& zd+R@Ow*-T^aC{QxB;SE?&HR(VIP(M^82K|RQyS2E%%er7RD#b zT~42El?k664%&XEsak(#tDAq}A2InN(KP(xr>*xjOjqY?g1*+bEQ6!pN)0u>*BhyS zKWnV^YI*<{<%~!_}7-HKFD*sgX&@z&{|0Y zgY}MJz8Md=?N}h}vH8YqL%U4aAR z)fQm6&ID{X8i2!QU7(;I1asR_aNVvB?x+Ldy-NiGQTHJdSvZYV+7L+$>Z z(DX;DnlYH%ix^Bi>R|-+Vlck&mx4RWLD;Z*G8MK=Bf_3pHgIUJ1sq*q40?<8!DOj6 zSS~vXwkysNN0bwY6_$PPB*t8i6HlstC|43dt z`u|@?*ME$`czsgD@L{$B9pxa7uxyeItetEDTc#Mno@u(EGD8zIW@&)V991y<>mZoT z-4E9D_5yzXZXhn$3FL*_fw6EaxGvfZ!o?fFd&znTT)Gw_mac|`Wvd|Vj{ttlWU$_+ z1o(qffqY~R(DfGs(_#(K?Y97xwg;Se2Z89V4tBwsU=ybc*69XdnQsIZWhP*L%oNNT z&A_z9984})fbk7WFn(qUMz5{FXw2$|@h7X#reCezn|-q!w)k!}WchpkSA+e5sX$Yo z11!D8;9|Z8SokgAOxXhzR}}Yns)M~;3v6Tb0GDbAR(Zx?S!xOvHRfP`!UD`%EWz}m z6`0(#2IF4Du=OwFG3-~<&)9MEuht`$->hF*eZ#)|1E^jz39!maK-@PK=xTF-qq7*e zrfYy>vjtdAdw|AP0VlBr5CgQqE=nJ4Qj7qbV**wsW?)%k0Tw4L!Mw#9%r07k=`Adn zyg)DA5!`pvaok6<&$zdiUvPs~UvbZ|-)wrZ-)w&e*l(T$HhYzTq&yW^s&l{%?B30CCeKAZ=Fy*7hmjj&cxhv` zf``)q;IlM-aNKpiGJTCcIft2zljE)4lQMAc918FwgmU{~`#R!~-6@Ab{CUS$_*;&B z_KweC|-=_>6ec=?S5o{D^Ra za*yzWa*Obe+DZICJLmYpxrOwB-sCh+KS};Q;J9%TIB!PY8~J}9)Wwh=o(G}YOCij7 zEreKa{URsq9rLBAj(Bpk2Hiaj`+5H6&t1Z?PnijJ51liJ_b3IV+vIY}O;SDWy5kw= zF2~FCtB#Ku7fHj6Hqscgnf#vBKzYxqrM_n!qx~KrAq`BFi@76z;fJ~yVTa~Il;&a- zzpwflWw~iA9KY*L5J}~gAM0qZm(bv`$jAI1F9_GoiMGGSN+w-pWK%9V7dfA&Rx>&% zjjT3u2fLNr?Q)v(%B7L|j#Eo}$Ek9D$0?(~b17wv{uXfBFbQ0c@0K8cA>WBQ2r6?R z?&!jw3HmEOB$%xqiN$Sy6-nIJ6T(pc+s{qE+e>2JCHA+uBnTs(C`f+Y1 ztCmy5sb*JmD_AFaWvmPQ64oPrA$ypg>+*)5#d*Waa2;_={{#PO;32Q&gYua0?NcFP z-%LnT`|Crd_M+h|!{vQh=Ib7%+U~j;=XCf=1WT_Yh;M$z&(rp#x4%QZB#crciepp= zQrV@vJZ=%UoR{x@)4yk_&K}J@I&UCfZ*fn)$*Mb<*d3ioj;iNk=(=aZ+{{nN#WuBmGKWfOFtyY( zicu&|a>;eia?2DH3)1+t?kW5hQ4;^UC{gfA6fbz~9xEIY#t4T6(SHD((Y4)?-wZ(B zHx}h{X?v!9$yJ#-Qm8(szeH>Pqhf=VH*&4FUrZw&YE7W&G)8mGYQx-ZD&*b{#r^@5 zJXts+%R8Qv=9%u6!4>ebJvs)1qY_TEMp-nVzIR)&+cGD23fl*nQ2lS?}p2b z5);$IeJDvGL5x^=6eltui5KdZEtLC~iTz~_9=@{k5}EH~#Gu$$HYoCyy>|DN4GDh- zxS(r$BhMMO0(tKZlfLI~oBXzX=j6WHJxYHcKQObaMtfODx!IPcBHIJCIV7#J47zb% zvMV+t-kp#X<4ul@44_AZg}VfYB)A0xWeR2TVzGB%y~H!HT_Op*j~Ebp1`dk60$#g& z`40(y2e_hZ`=CAV$mI%_Y@g&y+|hPF!hVYelMutBjf zWKiTD{Mua&C1iyojAIrxz2dw zu_~-`Nh$G2P9fDWC6{d(o5iyWPxm0nQ)JG*Nx^Kd_-L*;E=3@S$#-{)t`>8m&v>}R z+&~P9U87!$+#-gAZsETN1ZWKc7b!qI(viDdVYF(6!i$rOF?U*5DO@?TXIg8s#-fH3 zh8rsCtoG(t*{i3Qll9|^nHCWRTw8gr$iX*TO7+YLWV)wCa(JmpJWg`1keOU5q9-+r zoRhl{uiP1lLqb;Ekbo8Ydq9M|f5-v_NM4BfQM3p%R5u^<@a!zi^^O$^=h}BoZEjUt zaO{-9y5dI5U77Xx!->b7bR(qPfdCATfh@}{|L1ApD_fAHtJCd7iRiYXlwSte+jAgYhR+1V%UQi`*aIN&EFv~;fw6ZvsQ)wp0 zp`?^+U()DiS9%%of@@bi!X@S*a(@rVkoS+Bi}{p08}p!MI_7#a${#LF!<_A0h-vKH zGP&xC%3lQ+wO6E_Gu;}~hT9*~O4RT@P1Tn)vrGj|Zq}SeF`jWk=0G_fOm?h~rPgMs8(n8zhkFgNPa48)nK=o|#pLFk@~ zsqWq|sp#hZSsB+gm&SFXy`2~E!dO?AY=yNnFgDZ2Oqw2g#d^2zC3{uL1+pgp9Miz1 zgKJ7}7g>?pd~6)f2HV@U#yHrtrjx8&ipiEO4HWa%i-=ybS<5KZyy+v&{N(R}h?$t@ z*^@Bcl}eb4jY^m^=wyWB=!D9u2eUCn57#PYKG-uo{=UY-u)79p{BB!rlXTo}$JxVt*epF&=^ti#y@W}-; zgD20-3?7Y`={@*rsr%sjZ+V=F!E{zAV$L+6Rzm9}ObJRQve1M|^58;+=+~Pj2M_L_ z=`*OYP&BBw!gbJeJ$(SX)oFmRi!eahXFI??h#laoS`K)sn+*gUF&>D}G#pIU)*H;z z(Rp31tNr@4p4QNHL_azX`J0)7VNmw4$E=IF0&8Zy~ZCz`wTw#9MJs` ztfKuf_R!If8HbO2DpphfRHv@?xlQBnmzx@@p9c|deoI>=226mOs}G|% z9!-R-MbjYblp%D(9!S`@3$ixtM0KzoP`i0MoY}Gs&ihs0IN|dv02Fic26;egVXduW2QFf&OQo8bJW3Nt}5W>9R&ON`@v~}GSC+y ztGH+uV9JR!Wh<0(uFOPHDR}s8XTB%2-K$T2hFK_L2ue_FrK~>ET(S*+>9+?KVuU( z&D;R=nd^W%YYm8JuL9pWD7< z%XYwd>;bF517IF=2u#w{z$i}x49kvy!7)wHZ`1<4R&CI`qyxIQ5HEB=_YLAB;)m`J zJ@hw2(0ykFdc!E<`W^pjFxxvBY)~>nI56$sgotXm7%13PKyus&MAi<#i}nJ}{{UD< zs)Bj4I+*4h0h3})Fsjx9qXum-Jfi~!=XJs0h92nmAcpjR>W}MxN54KBfZnJv`WF*0 zcxeiTzXL4LI#}+W{BJ5jLg|b%N{AQ+i-BRa3aEsQK%(yeq5y?qvV(vNI}BC{8eoyB z3Fd`bU|OLACUv@C(xeB*?fPKUY4FqNvB5W^0mCmwV}>7%z8SnX2E!p!FzPcy*%ivM zeg{}?Md{Ga$?#7qK}E?3>z{;3ZxL`TRszdzBhaWjz{yP+9K2M(E=Ub*VvYbdRST?g zb$(fv=>4#$(f@9K!tk3}i_urJ%SN9}?;4MrzBC>+9W@>?`)c$W)vTUdfaxPkFuP*~ z=D!21kOtfBs3o&a2^c6D;i9$08U2ro)ymOb0CAn)X|MG4HYbWpNK3yK@5z zR#$Pq2dq&_W{(m>DuT0f3J6tZfmmw+h>e${1B%uI&tcn7Hhu3`hCt;L)kl5YDMV}3 zAy)S-A=TiGU9QoPZK=tiO^w+AuF?D@uFawkcg^BCuE*jjZp88d_OtaZ+)r$$%`e;q z6g#(}5y0O8Sft^fWQ2hd0Uk*4XSVu-kIkmH64I{MLiT}v zH+S`37n$}GMu@=!TCB-ka;o_)QjXOP#}e!{VvWsJ!b$vP!a2K3gl@ZYgje=$gmJN&|$*iTWTt!MmqIgv!h>QAg*!i!xX;7;drp zd9dB)hkjJ$TV5QsE|Ey*5?^L?jvH)#)+O5d3^U2LnVx0e=v+i>pjJE9QJS2NQ7(|H zDG$gM)M0WN^)tE9`7=3>{+XOZ|KjvJz+tT-u#p24uR`tzB_!cHlzzq>m_8n_K4&Oi zcj1dTljV;hZ8miWk@jBoW2v3@;_I9hdl{V)23R!m!m-C)bn%x znXghc=RQp~Tyi_sdShp}!=7_N^usNFT~Mb$&vlDyg?cnP&j5NE~jTCyaN< z;-{0-c?Gl-w<>xfw}~0gy~K>;_Aq1J-ZP?kzgQ7u$OVV5 zRzM#;6vr}lOdiNpR(hJRGW}Mz_M*#a=Ih!L@VlC#$%pF0SXxzbo>7UP#3J9v7nki7 zVxKOFaZDDckQ3ZHv&W3o>q3S4xW=R;=Fh59S zo)zGOOZAo8B}yY5W4#h7k&-OuFpm;uu(+Nj7q_zm#P`?%9&cFwk};;g$2bE){~f?U zPF#$3p99e@c+7HzuW73lhVwTl^ptK^yj`|;>Xo9S3)*sx*PP72?Kqa~pi&m+teGFh zF~|xNnx_PNVdDe+?V|m|9K(I%DZxG&&H>UQrk{5$+sC`rMe2Ra#alYWmP*H%QtvT_ z)O(!%JAj3pxCChkMo#?yuyxj9QEpM+-ZMi>D-BZ8-OVrz-QC@dba$r+NFx%0D5!wl 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�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 o{9Q|e zoaQDPP+j6ks)0jPmFUVVOPJ0to)IJ{jE@xN#m9?t;!>5e;&PQUVoPOdv9ncEV&^Lo zW0q>Wcc;T4Sk@npC%(B7I2Hy{UD zARLNT#G^4qh32H{(#%vVsz|k?($vXZVTva&FWFy^lN>J0Oo|n!CnZZ#6SJksiNz|3 z3A5xg5}FjT37zWEGuCNF&DgCKnQ%rsBH<2>&($Lnm|9c26ks<5j`K3{6gCsp2$@<`mT6AKS!1Xm(~--~oW{$_@a3mxgbGvBqeV&SiIRl0 zOlf>tp-OCOwLCg?u3ALO67{f@wHl!*JGDYmPH6?Fe23%j>LDpiEi{G6f$SY$VjRvS z;d+36Z-@5A;=f#(#W>dHq7ogu$4YY5sUU9{<>uK^R^B8oBhQVOn(NI^$_)}ERTrtY7~RDnDolgZw3 z9rJF;q1&5>aVWt!RQ%=2EWoiE?I;Gh#j2E5Y)Ba;BPg|GJSCM(;Sx$bd2z)7{Fvf! zVN_9^D7+|D5?Yum4K6HG2`H$O`xUgQ`4p^DOwaFE_bNE9;aTvFx>x?M3a@;oIz6As zr{^)*J1%4XAHy8j4XsUB46mN`mn+kRV|f+y@ptK!DwJHMLy1)uG^2Vf#a26U(bdy< zkyXCDu&NM2NM*Dzs4_|HUy&v8ttgRtm(N!5EMFjZFJGZLt$eGRTlq2YrJ7sWGgY@T zCZAf068&rUI0mX8ywmh{2vRI1ry`V`P& zMSd-KGiHkudACgCyjpxYkCss0w3b-D+x%3)l==BW=lNA4$L1!Kx6Po*^l2%?X)Jh&hg~hIfbTldXjTz0O!;h$vJc;@+Nj>^T&6V^X)ns1!Frpg`+!q zMWZ?niflVR7TGR&BC=V`L^g{+8xy}{FUEW`^jFWvJb-pHG~c;0$z6!S3OE9*h+J25 z%#G}+RSc!Rnr(1)yWi;zZcRizPIb!qrEGqT!dOP1Rvki|AvGT-J+X8mDg+Mhrs{W)aZUrk2+ ztz^)@hV=UfNU#4Q>1}(&>1}z<>278`-M)X=)B>j(F<-nC*F^NWya!{j0aXFe4Bm>J z5!6jB^6 zCbh$jq<){ zLe5nZcdiwsqWIvm2XMTO;}h@-u>Z0Qv2R)ZS7lB<`ke&ruzk=!1pT9k|8ckiCsCPj z20KH}!2tOmd0t0bsZMa2xyretS#ZCR~s4 zPpOD`#Id2BQP4k!oiXUI33RogtNJYJ0opP2)m>~J zSPA+80?yEBz!WCPvEfFj00HuTLKOfFIT9|wAH;xk@c+k=*p1&i1cs_Zu7ca}27ZQ1 z@#p`zAusVS@53~a->U1wi2xG(OhfHJ6#zK_Zx8{JK^~|8wP47dSp=4XmC#uSoj&*w z+o81&Zo?qFhzoE;zCoBCAzXjFbwYl@Z$AY;{|lyrTwe=S5ej$@D)1tN@E-{N15E_( zAQ;4hOi&DFLaPD&fd4@4&{+@L0N_ z(*vDe=xnFEh{bJqF}F~ya|4t38~hhvAyQx9lZJe&&)~zn1ywVjfw#VgA$$iNWyAw{ zzb4|L7>2QdTc+$luO*)JhbV-86~xm|f(&{nD5QJ*O1dkkrQ3ogx+PdhHw2yZt#BoM zEnG)m2)EE@!U4K2JWAI@7wD?^7G0J+r3>(3&clN_2mj^lzhI{DA54({8^V7ulA;m? z@zBNiXqwaSim~uqT6F?{I*uxtqwrx4YcM(p|K-5HV3yD~$0;&|1=?XMrPoFZ zda7?g544BVcbXIED-C!0L^Fu4YQ@kctyKC@JD1LCm(ppSS#(mTfj-b}q2s#kbWFFK zj_R$W!+Klkpx!>(uYZd6>3>eU4IXnl3}12mMvU9~H(PLg$J^6*ILuUQoc7@UaE|Vq zDbZacHTu%Ph(0nJMHh@G(-{*VI$;t<$4qC?VbgRvXqHd=&B|!6Sq%-CHFCSn7H~Vw zI=JoTtGIsiUT&-TE^f2=F|NUZJ*6s zX{bfe?KTM)*$oKW>`#eW?Y|MVjQ>T{4Bw^cF9h?( zv;Xo6`aeSdo2kgRJfIB*@$(^Pj`mEFQ@?{DZF01s^-d17#@T~bItOrFPLbSFry0Bs zr!-!>Q!amzV+ntu<4nN<#|B}GW2>;)ahYhI<2rGp;|_6y<1tCS<0q0@r=KLVo!*FR zoS6s+-{;p!xCOc4){D#fSzoO4{qXb?x4d2M$TwWzKj3XTYuv1;d+K;vI@OhyO!eUw zO%35%-C}qxZb|%Rw@m&#*L*>vYq_xAb+)M1rCB`NWr<{#%W9>WE?bqVTn8VM}yv(TGYb-6C zKABpubE?TJkZbgc;MIG^^XGV`3TixZ1T#H~h1DL_qDqejNx6HQQmOk2X|a2sa*_Lf zl>(0oD)}DwRq{PwO7lFK1c=|~kt^N|f_|Te_R(-WeT_i>aa@l-ukb}Rf)A<@e1}ns zpDoSvcc2EpXtN}#1IvzH99N?)oq@=A z0_D^kWIzo;BdIoc0?i6`<*I_Fa}_~Bywbo3esN&DpfDg+m>-ZM$_*$HX9v_sGX0yS z>HeL{ss8I^DgL|U$pNS2Nde!1-&K+VnG`4i$vf^~9$Z16k4GWrj6+q-48#C*VIEvjm_IK+G>n%U8pF>HO%h~=WQo#4io~fQ)sp0p zxzeQIB`OKQJ+k=V9rC!459F~SH&kOoep87JWzx7%rW6;#B=5M1alZhqqtM))h}=IJ z`4(83fNBJ^t1ePXGo!SqBFcU#U6>S+ zFHVT4l*C6gNMj=wsl-IAmPJMM%OfL>t42h84W7v&BA9Yy1XKDO367GlF%D;;bqJa} zQjq(nBlidGsaWgBW33w}qVhO(DvldQ1@ShN8$Xe<;#|4(xanMKTp%wwHiDlR8!wm< zn<|Wp%@xJOluM#w>ZB1d3zfrTR?0$Sw#h?cj>&^#z5u_-LSmRQkOCzj8RF9r%mu_? zANsy61Lr~(a=vWLgG@XDO+vnr$fv?YHOfsiq^zV7l#yglsY#Q$V$*$j z(P<&P$h2sFcv_+$G%ZsYoLVdnOr53VpE_Uao7$!9o!X}|J@t^xEA?aWM8zwGNvEeU zrN5EjD7lDp;0VTGC;Gk-%g&X>7>5$Nw7y9`7)NEx+%+{j#98-$T8AZ{#6Dcy+ zm5a!o&V}U!@E2)IDpH^3?2uDsI^yskmkR zq&zi?NvCEqB_NshH)nAU9KaZCL*Lhx!h@+m-dTych+`4jl8&~-7l|plNI{WBMigE= zf!T(n>6 zTy#b1RQO2Ax!{e&xqwNW^O^V^ClG@H^tTWCYpRj=%|h;7gSiOuXCm*a!nz;3rh+Ts zKUC?GKfD;f>e1v=J&C-l-N>ujoAam+;-*zc@upTM@m#C2`ID>41Wr{ALWim)qDfWj z#S^ObipN)75|6L^Q9QolwRn6v6Hh2-q6uYxaTs%O2l~1J`rUJIO{l}0LF-YG(13Bk zF%EZ@Y*290OH>PO~BWY@b9l13)lS_j;O=<8W=Y}xOu_2zD)R4iO&``{`Z>Z&u zt6wA-TfbH?x_&@7s{TXpgJ4wcE8(a)OgL)xKkUUE*n+;UsYMJLaZP9%+MR@Pn2&kT zg4}BXo=AhK3wY$*DksNQ9dcM`Mw1rW(!_-mX#7IFnPQbyt!}cQXy^Ucni4Z|4lVPjiM{-*bk`UUCLY8E4SRc<GnpD_NHXg+EhfEo9ame+fLLsZ6U>`6QtIAn-m-VAjNt{igk>8$2#aQL*ExJfd>Qa z;+3e1fMzuG{Clx8p$}Clo1woIIrw(u;5$(nuv>}L2GoEq$p_4U4ao*305?(@@FV4c zXp#tJa}12Yrr*cF=BkPdP3uGO0FI(E;W+%4lh~DV zhDdOpaHA6g8BoV-n9z<(XvZbA1fC85cn0i`Q5=q?=Av<7lV8E2o{f@f`{PQTWU7K3q_cc z_TV}IZFlr{0(3_}*ATiISKz;VjJ^+Peg$rT@4#(4OKmYVMOkESc18N|Y0S%!KEP*|6 z0|8*j4@!epKKfGuo!Rg{=E93;h0YS_bYV!=;PW@)bGKor_QLxZqz9-4xsS>7J^u9$ z{_8vVl-uCDzqy6u5O2K?1$+k?JP0vrA$af~$OMc6PGCAT!=RUd{$xR?2+^&A?@^1X zF%LQm;d^vIryCk;@%eq2K0D!i9KsYn3(wB`Y)Ic{0B`5Vj+a@ z0UtyKUW^#A;H(he@#yDNXa+zp8d@pP$wAvoaC%h3rmTZP6Li{ex^_ZiB{bH*<`Zna+w9(P`AioJ4KR2~i&nigwdc@i96q zxk87e-_w5O=d@RbS|YG}2sxu&fAjX`!v@d?FL7%BrY(g3B15f<7Ts1x4TkbKx~Aen zmt?+lUKT-Tjct%ohaI?n?$>HGij%80d3bSr+&Q}+Nw8~w&*XUKK-TCtKUN#4YtsFgMGBt z@GSKh-lUaA&$w=5#&v<^!x*>hJ@5rxZ;RgUjlyMc3@+t3;>B5Xewc(#qF(8Uu^H_f zHjZ|gOrd^LZ`xuON_}Rrw9!0;)|+S3I`cy6F|VZ6=5uJ3c@wR$Sj=@eD(XS`Oh0JQuCha>lLBrbbnVSi7=?ZyGmIw#@wW{0)54UhI) z%4z3tBib_3hI*|XXdS$mH8%dV(k6nsZQ^OUO)4$5$>BO|in(^%DsHiDJ=bQtfct;g zdJpibj;n3^IY*5qkPwI_Kms8N)KKre_lkN!sG)*D5+Hi-9Zc`dV8DPe#^3-pj(d%h z*nZR86UQ}8yKrytc%$Zl&`HpW!fB6W7-(hXj&iCZdw=AZCVqw#JnnKt$9VzcB|z< z2d$O{owZsLbj^BE&hHM`UYFM3HG#M1^TZM4f3_#7y&&h}q^v5k2OG5i6{E zA~svki`Z*DH{z5{SHu;Y&WNXNXGgwo+Y$M#O-Gc)dR8A+vm!0M@Nlr+bvRF_vjRSg z`oq}_)*d7?&yF=|XS9R1#EjL3*eO~Q7owGMaatCat|f7KrbTfjrUh};runfA=DD$L z=C0Uot2wbttY*iqx1JSypG{lraog6|OSUa>ckG(u-iCkMHpN?Pz`B<>j_^0?`y1;1 zL^f@ZN3N64JfGu%Oy-#Qu_?h`YZ6CkMUsb>CI@O!ai+o~mLq4mt9)izB@+ie??j@Z>FU9hW5y4`Pj@*m+VyXnam+uCG{4OsKK zmv_0uzlgn?*nWsh?AdAT#^AUIKQq~b@gr5rbm%k6dz%tbXdr8mjK>@tTwEty$S2YRit* z%S=c)3Sft zzc~Age#JQ!+iBUo*l_IS4f?^anQ~pj{`pG!VHLRsY{i$A_|=nVtzFKHQ!4! z^8?kGAEo;IB-Q0-nrib4Ox5{i=BoT!b47l$Rat(gb!q-$n`!wQY>V>u+7;&C-!DJ^ zdjI_VSNrGXf6i-*9oT}+&%8z-e1bYZTtPe3kZ(;V_lJ$uJdFXry7;WUXs~7$k5Xf? zyJi&oYkG0GYKr4kRh(|BD9$sL7nhhyi>uAkiW{wpi#x0fix=AD7q7F;E#7UHQ+&E# zR`E4>xqoKyryT!lmsM=B1)HCFnRwsC)+Owpo#g$2+pTiSd#tm{*4Si}?XpWRJ873z zezjj}`HTHh%0J=wlWl65#lR-*F3%Ox7qE4q4lRtSCuy3twa^!vi2?DLiEnk){Z&=% zqzazwTv|O@B{e}Ru8C4%O_B;~GEI3kg{It^a&vY~ojJ3n)hfMao^@)?Dx2i$9kxj| z$L$houGq!bJP#k)#aI7mn^0}B>7|MzJXOLuSxerF{iCg%53@KQVR<_m41P2algb%3 zDw*M+;u&L9SU*Ym_5R9j2v<%+ys{e7mD!MIN^dANr8d->QyONPlN!3M66%**$JKAM ziK#zo8(sg9ZB)av@S$x~{SP)#Gc3JC*IBF$9w!D5)w2c!`-f)J4qdbZEbgQq;LG$j z@=oHCH`87@EhCiG;--vNZ>6^eE3Gv~DQ(G0Zp&6;Td^sjt;!VF+Gvhxonww_U1}B4 zy2U!I^{{nl>m{3z)~DfbHX$wFTZgn*tV3r0!ehkyL2R6uLp#jleCQ$XgC1y^OWxH@ z-hn^qbI>2=@XMf`W0lzHsras`it7qjY*)Nuy3!TZm9NOIa#MI$y(zS7mN~dPH17mZNFA~%IEnxe49K?+?QrQpTM3R;}4z{S%vb#bly7q^;x7x$RF z7q2%>S-juuwfMZ*bMX^qkHznqJr;dy_E=!)#k0p^Hn>E)pThnD>}^|)7PgAqb2aU- zhMh4`0BH~nJm_9i*Eq_5&1m_qahK0pUwN+$(Ui5Zn!GkmUhDGZxvoMU>zd@gu3Hn= zt(M!mJ#t-l)--P2EqGhw*8JNvZk45vl^o##;(e4l--qpu=pf5BkpFK&3)@Wozl9iV zCI8<>{slhUS%a`+kS6YM(u5r@a@#RUt~>oSerKr0?Tpvhof&drfE~Sax<>8n(8!(3 z|bOZ zz(Z&N53@Vsnn~tI2Ow+?f#KYA7z>lYp8+odQh`DKMg!2X{GRW80Y3tM zz;^1sig?dG!kCQhGU}Q2AUk6&WB*Fu8WU0DBRmy7bFdqC z4g1(M*1(LujRtTB{e`buJ%{ZV;T3oleq)6u(x0^`gV9BXG9eqqW6CEmLHA{XNIc%o zfC{t&+TN6xGbaYdi-CTSra#`fJW=^Ov*MyJ>e1p6xj`_&7; zmthv+siE%qkD-A*!OoDv3~;_e}cCSTY&q$I>;v+KZAe3KjB~SwJkc! zAl9A`lOJiD-Y2Hwhb0GQzyjC-ba2a6prT;0Lw_)#Kghty``y4BA|MTlp%z-98w~wn zHEe<%uon)&33vc5!wq;EZQ>2`=1J@r_d!XkYQgXUw<4MFZU`BU+~J%J3fcc z;M2RupI=+-$@TlQ-au#%GLD8GGXea;*rj9WLDR4`9Xl=9>BPoDY%Hhbb=cT~zOoA& z`zikz<)5LcE}?zgK$Ca|t>8~|na}Au|DieAgYy4_WeBfF^N*ZhMYaco^N|any%b`R_v_ z@(rhyrI!~tK7S7uNAwSSGyQ_@VZ&>o$qOIStiZ;4Y;2|c-IRR@-QzU9;UcSpZs?AlLlt^Qw}{wHTJJGRH+svx^lBeNux}4^ zX^UNdeP?pOE>GrUZO&V)&G`fU;&*5jpZHVJ7FK6B*MW4QU7Z9lzb zHB_%#x#|^bUp;Rfsi$mG^@L5nZrPUUG27|7Zr7}9b{)EEH&0jkE!O3Jt8}UVCOz1H zm(KS;qz8D4%$WfX>HdLF=+vOsb;AC09UJ^#9YK3JJcKjp9*l<{f6MUp$vB1_c!R%R zb295y`p9zwopr~4l72NfP>&9d)5AkDblIU$4?0xnyhEKHaGa?#jzalTGEF4YOg zwK_I*n~n_Kr^7=}>Y&p_?RUDReZyYU?%|(lm-A2BF@oy@Y`cda#xl%I#J(4{r=Wd! zayf8AQb&t)5Db9Ufh!gQI8Yz~~n38#70H#`I|S znC05#vOzmsc4@oI5p5lNPMgPG*QRkVYQy-yYn|&4TH|Ult-i}Djt2jBC-y!J!vS1w zf>{5+@ll5RhbA%496Ly7*spcM)kBBf0(D?QjP^`O(|r^3v~yyKwok0qR`&*Nc5l;0 z_bzR4U!-;JtF>m*R;`}2-?VblY10aihfT{oo-r-;eBZR#^IOwGFNu+KE zMJ_RqM={T5XuTMU{@~9%*NgoZo=)29<*J>NeYFiOX0vyqHh5=hop+(uc$aCFPpww? zG-rOdbA~ zUOuAUzsK%Vaa`IH$hY83Ec3f?^3DMC2S4-&e-~|->ZLUSL0TCYqve6AS{j(6#ev0I z7+9&Ez&gzfY%$FZoMY+=TxgmTw8k_$Xoq=L&=GTc&;@gA@GWyo@EcY$L%uXOhgf=P z;@C^D#r!^Xei{3>lgT;LnBu^3UUQoa?S=VOu)S7?jL`B>cP$C?*TS#}^@JyCZg_^e z!t>P`UaHyQHJTOPXlf6iWoiwdXPOzl!rUCb#oQQvz^WnQ0jn7ik6G14zGgK&@*iNa zs*SXm@8vDx|6CFp443*#TxL&alj~%1pD~3wE`F?x?61XWU80?3h^1 zib+*lOpaP&iZnB(LQOGsriPdnQ+-UAsV-)zd3wx7b4|=%tLoVMttw+5wXTSL*}5X` zGwbrW|5}yDSq#j*#9GX6V&^I9ehu5kH1Ig=%VUmwJE8pPRUVaO0gIL+&>pB4BMxf{`G1w9M|#pV&0#FA1&$jYD{<5j0`u` zW_YVQBUqIg(JIeKQfWq}N-_#HEwkKIoLOfo%xpCkWX?0^Wv;Z!&D?I4opsDQE9)Wa z%&cduGqOH}AHZUjc@NLi4{j7N7GU>8C4I4qc7WC8-0vkGZTQiUGeEW3PO8cor-~de zmE{DeBqvhEISDGtO;=%Vp7L``O?kPsrkvcFrtI8qb7tOhb4K1)tF*i$)~R`ytW)xy zvQEzbt95eT_q?`P^^(gGo~9pMo5s{1`^Rd?{imaa!3th?;a4+$)D+mMyueYV1!GiP zFiC}l{wgR8Q(j@5atl+HQvGXyKkKEaY_?eoQAO)2f+sR}NBEV#6(ej>~tjTk8-buO6 zyRPx`ey8zszts3{i)nn9#pK%A$GJAHPn5qKyPKA9J}e{mTuwVc1?2E|5`-^h55!Wl zyq4O_W9cxtFLlwxWs@{vnXlZIg+iRhFHhIF<%Jr%yhbi;?i#&(kwz`wq7loF$$9xz z4PXAMhA;a<&P$lFF79Il$J508;3C!^U~laz#-%mHU>z}7PyP?-8_5455WK*3gN;US z9IR0rhil} zUV}D%BK!3}YVdky&+Ga)wwM^KBnE4V0XDifqrq(DeB6!(vxEF^7i|Dj!0kSA(A})T z*lVl7`v%K?-*64uHx@iJaG$>h?2AzUeaY&#FJHF%YGkvoLsolN$h4O)?Rfwm$Jo2@ zHJ#5Q)2=@DQ+GpyS-YiAgK5Q9^&axi{j7sINE;kN13JtakR#YXiv8ng04L3|J2g;pN?ucV&AKGkqM&pLz$*$Z(=Twv^ggy6Cx6Tgw*%0eqI2xQQUg>0w< zI{d=~`U-_#;mZ#_3m@^lZvh`*3-SJW9ZdZ(#v^Q|VJ{MUzI z|Era_*G-%osdFi|GcNKAHCM2YO*ca4{1_SlHtil)pZ@X;$LHXAcmeoTWAWq^y)pz{ zWEhik7bb@utP#PFKO{gAnne?E(*2Qs@y84B6ubwt$sfP&eL_w18Tue~&U%=0;5r)I zudsi+Zx53F)5PAe`69duzk@g6P52}H8Qy|-h{=0{Sa(89J{*Nc;ZD+oAD<_YJe06D zqz%>po$Ct>eeo>t#V`Jfr8EO8Mm5 zyBKRR4E^C_u#i{w=?;eWfELl~hdakWh=Xh>g*q^_m@ZgI`76*^)}cvk!Nx8$g8f)J zhE8w>ZQ_#tMfUqoGVp&8yw7>{DPDcTHw~Kx#=aC|O~$)mu`_agGzg(V^y&|W9y1<% zz*vKk%C`%tPZf3=vC)q9F_%n!5!%2?Y^2A8tA=YA?LdLs@CUJwV{2ZsyyF}zG^a2Zd!T%S_Kojjj*Pu)F z`7w}xJrtYcvFS^lBC(f(tvu>chK=di;8_^>@iKnAh#$}6$FunHw6>u(?A0Ci<~+`G zQf{KfJjP$w`PMa}b(NZ3#md8Xxx&$)SNpKVK7M?QA72xXztcAFvPS4ntj+m7{o+;j zYQ2aS^DJ%h6q?W-`o`ny{dw3~jGfik*i8Amb(~lnH9eri=4(1+^{fuqys!PXUu!SFrnIL&JG<`T zM}`#R0Yo1#th@;?^ZGgbdfIM)Zd(o0b!#_0Z0n~>{UXs7$5yJFV2lVVkvn*j}w0eo||OKcv;pPip0ex3qlZ*IGKt zq9u1(%+cTrH-_5D48^`&cKpdTe3)l@l7HjZsS*8j)Y(Y~M!0GBC|~Uy9jT+YZ+XOIAuL)X$7PD|l zn0ltfY2K7Hb$jQk)4N!6yerk=Jwxr@t!ne>Qj5=0&Ggx*Cf~iLM&Hw>2ERv4^?om! z>ij=8P51u^EWhAqG5sl+TOZMV8q9-9Om*NuJh@J!p?@&H^XsQYJmt8@Z=AaQC#!R6 zkY-Pf(yXaTY759zOF)5|14`8tP@{%`Ce;VdR$bsiO%Ggastw#_st!6~stUSdstkVC zR1y54sXXLAU@?^k|HALWnRa0FY6^WIoi>0yspOozw~E*-46#;skb`Cik5+r|B(;S2 zt2rc0jiGU>4^7pK&>T$+wX>qA4j>}O|+%y%&S1UihNqO;eOu6w(OxX#WOj!vB&6x=o%o&M~ zo6{5DhHt=PU`kKu;~8Rb4SVOXf3%o(fXz(5R`B<{Ox79`kGjNus!DKFdEywACQj0{ zL_ZZJhNvJhMtO-z%1g>pZc?Fgk}H&zT(8XJ4pT2wKxVzlO#^plBBkUfia6`#|pxSaW>nA|m{=-l0=sNDNa5qXc9Bl2E{ zf0-h3Eq&xzOp)-bT>1i2&J)<*SIgRiI&>gdP=gMIU$uNzjE~v;%2r0+P^IO&C?$WA zlJb3($bPMaf@sATBrC2UTd{@3iY}~HRAI9s3%eCww8|7#w96DyblMbLblntG{998{ z(U-ion1Ty`;Ze#z$8_`PbW*Se`a%QAFKN^4lsL^n@zX{suEb3- zB~uh#5~!$>2t}4AD55k&;iUx%E2~gQS)+o>Iu%&9!W2-p!{lFn(&Sfu&E#AD8~DQH zS7tH!mENVqVtSZ9a6k1wgx#GjoRh7r2ZGL-=s>(zHiO)o&y%XnimtR*WYsW*SB+Iz zm4`yA{S;gss-Wsv1y-jjpe9#SYf9x`Gef>L9rCGNswuVGG`aS;ylStSJZoQ;XYJ=E z&l>cHYKzIspvq#pNIg$d|NYqA)nR~zZ6H?z_f%WJQ^`qq9 z;3mI@$?|OokWWK|yc-iWr7=@pjm7e8td&Poo7|fgX=2l6O=vnI*QP5P-}C}}D%ZyU z%C&)&!}S)E8=Rw_y=!sikb8I24)ZuCp?4ih7wcd;$$jxD5}&5dveM*sdwI4GlSjLY zCe3n}`z#+#oE5AIvts1dkt)}Ye2wd<)Yy(@xpd6e=#KRoHT$4O&b}n)+0V+k<717O z^`l0%vzoiDkJHq%cR$p8&WDAx!(!S28lZF$_y2h>ZUHd>?;i5c`F0vRe~4UqoHeFr zoJRL}XjG4%M)rhh#DaJYUy!L`3rgg)pk6~4bjguLB@PSrX~=>L8ob~s4et3+gXjOC zA@i6$8_c!n81>vk`J0z;eOONZzmjoj75P8pLo$SeAGm`H3|rM-j;q-Nvf3H=b<)+I zFje-eBOqCWR_ANr>T300-L8JCm&tbZPT8!wUskJb%6jFW;a{>@!5)z1eH^5oJC+fH z)m$IevL9geczK2lcOU1i!n znTvfOg>muUk-=+D>g{x*?F4u`g>5?qk8tw;2J!D7@E1Fo{4%l9W-MQFl+D`tv zlkwm_)&T55gV~D)1mpH$e?RsQq5&Kt4?WzUfpsuA!DyI(;O-3}z(wXrF3{nP2Ou3L zxQ8j=@PnLy&(ioGF(7^q_^_Y*Ya56GwioQ64R$jg@K5uwlX!%6I7itBg8So7VE-f< zz-jh@oDmm!IA;w506)&*$9eqVSt+`J9}k8?G86#=&4YBL3)_GKF5KeZz0G&NhM$1l z1AMfYI?uv(?P12FxRuh1%H1N{s5oDH@zB6!ztF`P~SA{MqI`Iqx3&)4#nmG!=_=2X6)5nUg1c$A_<~A zcpct^Kbp})`k_$}llPoh7cv%oVhTi~QRG1bH*%M-UxrG5O27D&ihTA4{7p^7yo|Rp zupNb6Z|sh}gZ@SR`ae(Egzf)Yhw>JGzYFid2kL2IYJi{h2Sa->^8Qia0md4P1jvChm;r6j4U1qUtcR^| z9~^+=a2BoLGTOwi&?a6W<9`p$;HzHk!&v+AZ}<`n4dSox9`vofF!TolLyIu<7&q_( zV^2;x6hSpKQT{B-pGOA0nDSRq{szk5hW@deeEAU8PGaLc+QcJhA5Wrvyg`UQLpJ)6 zaP>Wl<3o<`f}uzB>JPozhq3O#7Jk+r*eApqjJ{`rBw{ZYJEho|jutVK@@G^2d`ezI z`Ku^@1Lbd{S@zI8hpF3X8GBLQBJ=+f8Tx0LwWU4FyyC3p_{GzXf6rnJZ! z?1`Z>-v(G zqGmh`13#X{kEdvtCux@_@Z)j(xP|`kE3~N_w8=FTh^xfn3SM0%QkQ7C2eD?TX2$X7 z*A}|5;lnpZmjaoC)K zy%0)_bI=WC07sW#bHYlD5g*4wvg?ch$W9=u4acz*7R!P~WL$RRBm zf=c9YLyH_<)dI)QG@oa*%!9c+r{!lp8pn|B!Ens5dIK(bk^fKRwgN-kAtx*C9pa!J z4r8>%(Ni0T25OyCl-BU9mQ};Dv|?C+mJKV_l3_JkJiJj0hqtR|_*~6*UZ%Ouo76R8 zuR2GZ*6fjwsAJ>{nl~lQkL$1O5JH|7|WDnWe zku}s3f_;B-K4P-Mt)CXV zqCdEe)!Yf5=r{qIJ(1@xOiWPQ#B{Yx%+*ZyX=-w>RHJ*n>L;~n#-w?wLyM{P*rpnf zBdYeis7kLV;aycs{#NCa*#Y=7UhJO>qz_`-&|uEQG1Y|5H(JYR{do5KTOHg^eF|ZnNq6iDYdHdo~a7&E|sIjl=*B{iSI#8 z^F6O(zuPME`-=+w{|)Sh^!tSuvGptLUrZ#=NG1lbJ%RgM_^~*Qd#$uho6jIM`wmxw z?|9Yud8yVfK-GQ`s`8Ilg@3Bb{Bu<5U#ya;Rhl-nK}7)_Dhybpf`ARm58S8Rzz38Q zbW=IOe^7StSAY_45Y)%BOfw$E{<&20j11=3usMww@LmtT&BBj{!2W1ELscE5Mt$_uGePDs15Lwl4Jx>lKCdz8)t^3uX@n9{<3 zZ%PgS5-bMlCCs8HxwJo=LOW#A4msrhupx`wA3wVBqXj=|!)#R%>Y&oFktz5(l;i=3yFsMSi2x=%^br%j2`kD3xyz>($inoVp-;ZzZ0R&f+-52A)EFKWDUqCAxq<*$tB zP^Cr3K(bO}vXl~2tmN1#CB-%?F|J$jaVr%Uw^On4ClwQa%@m#RD*OX1rs()SV&cgC ziNS-|JAv(e#Z3RFk#llfz~Ak}qmFo#;74BkAZ5onDI?xRY4Pq#jrUPQq$Haz!R@S48r0g{NFKg{8a%pMk{`mTWPF8zk{eDPqv8 z17UkNQ{c_zoR1vmm$2TMcvRzGQ93zaN`Iv$J1UvAIf=;=l#nu6aVY_cO$k>_N}Qro z(-f7Or^vK2MWoG8czTCI)0ZeDeXD{qjw&eQiUKoUP(a3~9RI7pbnX;E5Ilm7v()=A zws%$07pmzC&|N`0;8$fKIUntmnrWl>jKPY{a8^tPzsi~Ep~y@>MP!C3JS#?FS;-2` z%29B3iGs4HD=@oV0Xd5_HD|N@at_Nk_p*F)pM#I(lk<~&bJ(q$-G}jP{{20!=R5Ag`i>@+`V2kD_Pb@9?8M3we@VK_3rN{&DKR7u#E#=nKvC z1&*^C(I4<-S`F=hFLC%1QqoTWXfabuM#!&ZynIVM@6u3BDP<2vDSI$V^W}jS zGpVdu?q%~dv3#A}%J<8){DQ`p{~G?L@nt{AwG92Cl$k3$K+KO||2}MQYU|qzG_Re! zkJr^R$-D3+wVrwZbaLMsD^01kmlu1KJgUcNQq4rU*G$pGnm|pciIiJSl3Z)EHNLh~ z<7(^WGQCq{rmxbd>3cMC`Zz4mrb!yzl<}evJcX_6Ir5aexsTI%m8bJ3Zyl=?~!=+?fcuvhS?apw1Qz>|CS) zomK#-^p&y7wR{gS$szyz5Ai&p@l8vd{{#Mzl{7J%E4HN62Fvw08{wP z4Mu|F5^D`wGDriKII91WQR=tEO?FEr%XUeite3>dYDv0GOXz4zXv8H{V##_s`eC@j zBEXlp5Pk|jfQ3r4TE2%EQ10p#ytTS-FHp@|`oTK(!SK}x@P&ym8is=X29s02|r+I2RAF4(A{m5b5a{!Z`E@ z8o)80MR6SaC*hO{>;OMb;m2wGIPHSiIf)CPKSV7f{dEJ!ml4&Bay%c7m|uaSHqQ)Bb1C0M1D!LdoQwiyS=kGd4_s26FKo!i^Z( zC{WO&zlOg8vAFi%J`H9+*Jo_!V>j^(8WjJ`1AC*f z=YYNb4`c5UxDLO9TW}j5zsnsnlk$FOBlaW*L(wWmp;h3=i-FuMOM+==2(3V)zkHa# zuW@`8K8A04HJG{+JdYOJ$=D60exBGI%|ByGF1wp|?L5ixX?Px9y2~pZDe8aqkTS*!^Agrx}mWGZtby9J^kR zVV{qO++huhVe>gKZ2ksbhd;ny;4PqQ{a;$hNBsQ>d~S;lGT3;40Xhi%B9`Xd$m$;iHqy&s0|U|_5}F!q24Bf%dAXp30tILez z=ndhJ40%wFjXJW$7RsMP2HivXODTUfaL-H!ILmkj?1S>^rMxlBlIk-xvf9siF| zAbuc^@3Z|bM`Invt6)T-Px~-&H1cmle=y=<=s}aHQy_JU#a0GdL=m;&d7$LEpRp$8 z6LnH@59KeV{MFQJquwQ}eVa`8FJ!;QUX(ZK$iJr(y~ew*Qo_rWYS=UssORC=VCWHj zw)^sb{8!3Ay zIr{-DpF)?oh%RxHQ|LGR|5G%BA33#{yTPOI2=uPO=&k>~8iWlthtSnV(UOy}6@aa1 z?4)8Nk1kqD`8*2)Kb{~Sw`rG~`0*Hi+`x}V(I2jo?O#D*xQq&Mk-sjW#+;+XvnXO` zuzNq8z6Yc3Kf;$_bnAEFbz<={v3QR4D8KGqgAqc@#$h9!o|aFkr3_Iu=mHIBDJ|HT zjg5KOSVH-0h`<)~h`qGP33|sxR_feBnfV>9^pD=2yBqs^?%_v<+y)l?j$B5Ixj>toLlZi~Ua3=PQz!7_80~U|{&5ID4&ujt;<1B`KiZGsm-FNTy3`qs z!JY>?P5^cyv6Dm{GKqEpEnmvnP>r2>?6lAdbFkAx`74OVX3pHbygJ2-t}A%K)DSI+kJB1=|d>Yz@_EhTUT}gXmp61Cut{X`ZaDRzccq z9jy&E$y#Tdr8Tw%T4h(Nm3GxyZdb3R{aUrSUzZm3TcQR1*J(ccgXRu6uI>Ss)j9A< z%^CELX4`+G4)z7jx`+3$`_e=%0Up?g2i%$Cj$?kuPAKJFHTh!*;be9??vm)6z8b zjv9x)qXwsMR6oqp%M6YNZ(`HXV6OR5e}C-zl6!kGzns8SgiGKmr+!-MI8+OVx@f+W zr@DttRp+p9%^n`7S;JG+K0I5k&g{W(E?2X2of@56)G(q;Ge#^^-N?BIs}n^>k|_c|51x2j;$eC1DCtvrwWlZEbCf%InsO#rD|}?h=Y&#O zn404Il2ZIWQ!*NiL5go5k5lJ|uzxyF zUim(r%JcD8j&G>4eWR7>o1_fiOr`r3D$TD#ss0U0@t>olsmqi&b*mBrjw(LzisAyF zS6t91@LyiDGV(4riT_1xo=Bq|GMHn->U8FJ$>bUMQI8)L_)$1@kn*NFDSN7mGN-yL zBfwi}0f9;lh){AsypjUblo*(&1fE?JA2dU8!Lt+_yjU?In-v{;SW%&u6&d!dBEmj` zA9>Bn$k0Bn6Z`Y2=s;Oad2`73a>@O(X$Smh#kcAB!L5Dto&L%Qc2ru(D5ZqBDJjHD z39QeF4+&LVXpCY*Qxp@Hqv)_{iVB~u$nZ8rL@ZQz#72ch9#Tl;MTJB?t>CB+;Xk~# z@LnH}VCQTmeF59M3P=SCndj$|`{NI3o2rOMA#If&X{+RjAxemFR$Rn5#YT82I>J{` ztj~#zj8a5YqQavx6&78j(CBJ~#Iz_lrbj`s>lF}tKvUx`$UpAa@K^YOznLY)_Hl(a zIE}pn*xo*k6r_Z-h+`*zHxQ3<+9r=ak`iyFxLA8d$2uu8c8nroCn_vOj~w zp2xDff-wO)O3{Jvqm=mM&`wGC8ksaep-GMkP8zA8Bv%C{dnzE=UsIDq<)0EG-;`AO zq~ysvwOmtD8#FnsOI~TK<&m~WlhPlMd-~&=nEo!uZ#5~6SrT}_`8=)*)O$B}H`g#O z)uIVClbDzfNoYAW!d*Vwpz62JUH6?$LJXsgxkv~e4@?GW5b89B%`)fi$nA{5D|iaw(grF=gHwT^6KKWzia0mLli! zd<`$J)UfgvIaMsu(2A{ctT-Wuit8F&@rE4AzXG%ev={^9_o4PQF)p-meVWC%&_Q3A zjTXi0X|0?O_!Z0NLHIbCIJwrD&{PI$_;d#in?6EL)5mG(bPw>8V_ldW>f$w|E=z;! z%4A>Hq(L+0Yv7EH8c=sc{p%i9zq*&;Q}v(zlLpkXq8biS{x0lo=%gLG`_`ef&ZQlA zo!3QQoI?!o$rs<;+sQxi)v3)|gIfp5zICVuwT{w2o}D|Ob+Y=m2C83cwCvi_WYbn8 z>$WxW7^`AdL)~rd<{NdmN?o_}BLV*j$Gyu%2?PxrEaD)*s4m`jg zqJWcm6&-mcjkuCZtlWx`lUxjc1tf~gzGmSGKET!`#DKDmy+9r77@Ie6E^J~<+KdLX zg?%vK1|z_MV)}2x{&q3Rv7#gQhruwEzemD&@Pa^ygB+;la~gE}TGpZ*hN}erH9Fuw z84UgltTV>;3hZ`avjH0=JLrdmEou+v!Cuw@!H9jte?MzL4q*Eb9QhgiID#KX?783& zkE6un7=9d^%w!}4lAsvqbjIeRqZEAf0{@;qapYqLh3|nUIbeGpb!*jO`EjKG^n?E?AQ{czjzUUP3 zByh!O6>UJjc#q1yN5Sv?20qmE6ZApqoP=w^)N>N`96{aeZ=pXuL77j%bMPYYmEPx) zyvgx@;7{-ucn98t58y-inD+T>APL-1o;fm}jRXPQ&`xDjK^2F-o?oKjkO@-!3QEB1@fUBjI|)G&jwh*7${(zUPsC$kEV0eh06BMRs854rZ_gLyH*$o)83akVy%}=mOQ0-$40o zl-)(y3(!86QT7_Nk4>iq88D+0QE7*)&wF|xA z5Oq9F(_N;XkE46MLAUt=Z~E*$&C$?4jAvn7gJ0~kYzKe7Wj)H5y!6WybSE^1`8YtRa|=nk3pSm8UXV!g8pdP;13ByJL(B{y zpc5P-`#(!m9;Jm|7ZQL(I#ZQpV|joBsJIv3Q8}C>P{P zDO0c$Os%4^k&KNjY!qUn3>(#G1NG<;E%eAa*yy3$74(cvs8f5;J5CUp%cw-`10ljI z=*`QqdoTYOz$wnq_9DZqF~nWtw7!f#=Qv+ZagH2iZ_oj>n7y2<_puga2Yq83eRKErw);#?{eDtYf1dtx7ej-2+7P)x(2TH9Sg{&WS2_PFI<8 zo=QfPXxfMx6^&?C;m9uKk6f<2QQMR|`k1muKcuWN&%wvabomJweed!l_O1kTizSR0 zgfs2oxSqe4`ZN6_ChfFOgNq${j)N*kk5u^>Hs)uaMcprA`$RN-FqUan z3^9l#-(wmydn$Rx6t3;=W|g|ytJrmz3S7r3&ux-&-FWtaTadCQL@HxKywWG6DQ!Zo zQYV%wdE#^>xwk3NeW4O2ZBU%Y0mXV;P>ko3@PT5yegN(Yc=loFK<7*Q=FAFWtT$fO+Bl?skapn@HTv_zyQ`BOy$Z9SBU>YGWv2a7YvGg4T4 zfFG6kQHUSu0oF?Nvsav-lVbeFD9UewBK#*S+}~ee{-Fw;8mo}0sR|CrQ&2#;0s|Wq z5ZI}yK`Z4Se4qS+&&Vg_Cj1%xE#DB<9R#yd6fPxjdk_2jvgr%C^aWU$!#p2fX5dE| ze&o_uQs}EOK?4;LjX4riq(T)@t;Ec!woQ~g3xks@+`j_v$iOFT+w zn{4_>B7G)2yq|)@hbWNW^$rLhEB|nJ`GxcRg@{1;M6mxMGC@<=D>XT)SYA=J@{FFP zNzqH?9FyrwaUzlAS(%?|1Wo@5XA6ZeDI+fjxNR7rni+S$dinZGNGIUk5gI_(of z+XSU??>%XtJdzwVDal#xN#isznP)&G`^YUhSgtA28kdryu_<|SNv+VB)R`KUx7r5cvqD5sox8k(~~ zj;x7s;9-V?b6?hwoPY2d?ExLe;3%=*jeX;{nO8Q_7n;x|p}v8>fIpe|5=UG@@Wq=p znow-g=)wVVE_BfFLT3#t94n_no`YHFBgevEh?N5iiG~yvYjDvF*%x?n?O;Mv7M zC1+Dm$0iCt4v%u;ze%wF1^58lx)~c6a6aH__fq=eGR7syf>;Oy58iiKf(8WkV9VcH zYJ-?M5QcygFbQK#3Y%Ran4l616GbU}NA8uz%+{t+WEQ!@Hu)mKr zAQZvmmXjIw-#zXl9{Y&L{=tAB2k_$nejN0J7+x~~9UvA5y7?Rf`%m%jZ&A+s@GX-j z);UwJt}T6CrUX0b*oZuU21MA#AHx1&)}S0k12~4w6F?CpE&n%X%v^l?0sZ3vMlI8CDERzUc#VHYdz|}^=27Qn>c?_2WnwQHTfQgJA+hCzt$}CJ z0M23a0$hSC@bFzIisYwH7kQN94Qn(JVsg`gBxNL%b$8%AxswK!&;jnZ zn_$>9Y#DZX%jfvpyS&cvP52}H8Qz9>;ji#_TM|h6$QQKBKd11VkT~>^Y3L!dfX??d zmHwLGeQjjp#`+6GcQAB_LEs23;03{u0NF4NjJ+t0&<g06eh=e%2riEyU1bMu4#% z#UG;3SW>Z(Px)n(T}$@dM7ixK3SE@D09|4ky2n~F+AZh>_pw*xFsjEHa_B2)6HlTW zyhWJ4?$thwwI{y;!?v*(4T6+f=fE)UT^E|G6PNZ!v=7Rc9^qZO=Y2-%5^!`L`S?tX(; zuc8Lkd2K(Y?NW68XNUkZN)|> zHWqTGtY!_ycA|2K7COf&m7Ay%Z_so81%1k*p)MNy-46R)cAm0l7~*(|;pG8*I4K*x zJ(xNT$KH5a*b6%W*oeSJJT_9Xkxh*Yu~Ui`QH>T+kDWGbbfZNqMe{H`+Cy|sv$E?E zdhm;U`U#%04+Psk^CyPYXNO_mnY?p2`OZ*sE{2x9__l+wV-s4;TH0i#+^EkK>;zIv zo`Z=W^YEh^Kf2IC=HN#M?J|r0(Z;J5{zWsfXe3(owAc*vpy{-7EdyAs={wb!S$TGs zR~b%kk3|x9HwIf1S$V^6V3>l`W{OmcIbO|H z{0@dywi>Me4|{I`U)8nzeNS9Za^mjp?oNmRF-U?-@Zj$5?(SBsP-rP_u@)*&gVIvB zOO>{l`mNh}zdHgs`-I;A^M0Q9{r{imCVYPEbM~GyGi%nYwb#sEYwa#F(6op2H?5O? zX2Ybf*#zlrK1X_*uaX`XJEfcD1Jc#oVXWk+R9@uB1or#QQZO|}FXYw~bGRQha23W^Poeg_H*kno{n|$eITOvJe%cZ+*jdatn z217GQYBXb{i)MyY*)5k!yX{h8cU;QspOP}|$I?mrD`5VE1K@Zu6dOcQ2XWlufwBk4 z;t0kR!N~Vz*o937J6K3RJ1sdJH|eSMlkVCu>8g#D8f}VHYkB{JLp!N*D3wZwDyeYj zCS{Iw(#dgzba0$1rA|wv*lCLtIUkkw&QD08%S|bC{RNPJXu>(|bP19Qmq;mdNsvyiY0|+pS4v&mONm>V6uGhf z!L5%J@-CwS_lc74u|V=XH%gA@A<6bSFIisiN|yIel1&b#8IQ!%4pQ;M4956b47D=x zfi%XJiNt%@q<bMp6Ra06$1dAoC6aSQ!aUQSSSYyDgV`$YY2H zW@Ix|PG@X`9(~ZG20tkaG?QXKJ1O*YmI6O7$@2@8T>l8k@sF1*|5V8g$dQbIB1sP{ zm$blck{UEXl7mJ|Qt)g^3|=D%A^RmRIV4k(Lfc7VXh%s1 ztC9Gyei9cxQewkrNOZ(1iHh7Sk&&k*BJ#RKM13ofQ98ae_W&HH%zKc(v6x|S32_FP zT!b%ju0MKoLyrpdXpbJ*5mu5Gu9cK<7fA~Dmc;NNi4TvExQKX(WAB>S$UKRODwSyV z5sr%PBazX=B|K)DgvG3o(AeD)68o?O$6W(oOGq4Z4?t)vxqEEC3z_RXF!b((FO<<9 zI^qlH(ic5y(4!N+Qb3)h$C*oFj7H*PoFq2JLtpz ze}ZJc)WPDLI!V0KmWWr{4)IJoDIV$1f!pFq{>3Y;5eF#mR^+elOutx-Ki2RFn&S|3 z>4_dy=vRWBa`2HPY!aDaB_U~c5|rjFf$3fnkRB-h>EYs+9xuKb>Ee@7Al{js#51$2 zcw`L}_pI^anzcw=vYGvt{eU>-TmfJ3`A^RA#E7{$d$Io}Z=U0nU zew{cLj1`B1`Jydg4~T-}V%P2&z)aPGKRCzJvwU`@qr4lCx2z}r*av^?i$8K4(2M*- zH`*k+6`)IM1#u6$1eG%XpvX$x+S`juduMSj;yoNif#O&cAr3_eAWO8xMPgrEC3Yo! zMN=|LY)j^dP00qaDmg5cB~O44`23St7n6Sg+mW#rc}wc(p9kOz1Mx?W%37Ri^eI8N zY;;M4F1!n|7dpFDn36xS75g&s4`pt^S2X1zVp|?7HsxtzUEWTt%FD%~qNkWw3=`9e z8DdFjG)JBSNEe)ua4yTsJr5Lj7U_MU)E5Kgx zD2?qk@EwKx6EL1b{&Hl`9Z5JontQ|;`X-KD`QBkTzR0yi^b3QP4>odv-VVL2>MX^i zPD8@R0k{EQ5DMZzCcu&V;oSWo?mHeVq0`+7PEo+?;2Q!wbm+wz5acXI?kvbAO{5+s zLB_Fi0yf}W>R94$uKS~x+bHHhpqn)?9$`+hz=j{#183j~0+_@R36eoRz=?)oz+wEb zem>aB4d4Wg?gehfUx7bxe%64DqdiQf9;R`Rn1L^XT2MNjIS?QY1aZw{DmfrbY7I<) zOfd$QI5D=EtOYK>3j~2!3X}((U<=S=1A1)m;RX~AQUQLkp&J+hDBSv;@W3zEz6HO30iN9BDr1q`A6ZQ2 zlA;Z?gH7asHj{(ef_$pbY8x_l0Dexu!hlDGeSF_%$_?0x9Elx+0sP}2dK`)b)XAYT zzSjXN^dJSe?>KlK+S~l*H<>{B22r-I$Spxu267^iNgQ2d{l3y>{V)$j+ghame&TrUNo99)VZ0OhuL=<5}&vGCILqS&PyO z-u>Y{6uzU#uqf|FnMTY!hqzUFH_B?vu$ipKUQBWv)0`(a@d~+vZ!uZp`WIyliXvOd zL7W2m@>wRtdIMHAQXWt8ETQCC5|EPx|04N-c>6taLT{tT8|d*Wdb~mud7TLC8d2tp z=k?5ZS|6FEsl)$Y58`i?R7rgt!dkDNo!h1aBno8bdE;XPlsz!E0!4@4!MEU3pt#jb_{Fp2#-73!Pv95lnHTdIYd}s*KD9IpeVXL{9_A#x|w); zBYBYZMDuIWV>KComBif3=~$Pd$zrr!2-o@4{B?nW3NE62$9H0L`Eh#5BB1uUY6k>OUOSgCJ(ZZ{KI_on1?;);HR_rHvy)h#1$I-gw3Z0dw3gm4W(P7Gy9Mn)!?j(p@4 zBc}{GU69iq5ABDXq2xTq(UZ)imX@)yY%3molv=w0#e3if(6|e7H;z|a$v=3KM&}l@ z#~b+`jP1DPEVAdeie07{SyMLNze(P7I60wu=8p}=M+c$D0NQLFdh|mNRy-i98ZYe$ z{{dKKB&}x(XBMH|MyWD7D3wNMq{5gt4w-%{1viGHRim7!n{N}n^#DcMXgj= z^p$c;)?iq&2E%Hel(IIc#QJ~~SwAZ6ZC;l4w%14TUy&?_?*Lh7kPS{F|4bMqaFB-gQA zavZx#wqt+EbQ&cYJOoQ~UL~o{dnCo>VM%tmCP}VeOENi_W}M{qzCW4AJ88uJ+}h@+ z68rOgH0K9nlit{=#+Q4myQvhDi)rWVDEZDFlIQF%IWA$6?ZWy8=Eh{WW=gtiJ4tix zD5-AMlES>0B=_Nx=rK*=J(f$H=Prr$d`M!vUI1T840)JX_J~n%oU-ph_QovgA)9&t zlQS4HqsIX3qs#$eyGiD?@QlOLTC&N-WO}$tx`&UXdIm{~XM`ks#!HfCnk2GsOoCU5 z#F2}M_30%s>=zU5J6R%qmr4W=ro#PBNSOb#66*g?j(aX ze1^Zd)B{7XekqKv;t7kQ$UmS*mcNCh`r1j7uahMDdPuylzr^{5N~~Xu#Q5?42mdUI z@^3GZ0TmJv&|Sg;2TN$+LmNeHr)*gyw~4s?~sKp%++43hAmNC^u{kkH@^ z2?;5X;E+xd6xvk+LkCJg*m&^^TOhvSTf`^)n0Q4z4Q`2d#P8z68Yy4q<{Y5RTame{ zgdunb#@JvipNF7JPxRjXP6->tG2w_fBwhp`iDTlgoMZMb*p2-+Ab(jU{@59R z?7}1ID*Tb5Usv=fFUA+JQx-N!pl-raO~fzJM!XZX;+5zk9*JJ!o){=@NfF|jlprqT zVw{uPi&Ju?IHvR#2OijKQ)h{N>N>Gw13^vN1@OLTQh(w*BS)|UIqQ(Wq?*3DYh!3O zyq4IX@3ov~I)$`@CLLX4u~{%S@y@gq*K|8^Np}?Iba!#e@DoSYDLG`siZ&xn?3ov1 zmsuv7tRA3VYzP{yvsQ^!_I|O*J_BAC%j|E(iX4nJ*h+a<*Wi!c>6?1tkD$IMF&E#f z(S_F#NiHYEto7O1#U=PlH8obQsufZQ;oX@&iVfaYL$Lw6#Gzj>be{dl!NAhCH**kro0Gq=p#j?iC%{c#No@Cs z07(ER?t#&ii9J1Fs7$KpzLBKsF>o1&{{n|Yhg#kU|cePGeh30T1Kw zPtpnB0>9yS?3{?1=1intm_j{FGOFO+ZVi3G zI_BZ5CkM3w`{PFD{EQ%i8w=PDcC-SDC%dr8E*l1AS_TB@u@~Rin+%Epw%9WkAZYhN z@D#M4lUMmw#!`;{$gM_Zd*q~Uqd!K52R!X}u?}Vry!U|vfS)!z{5;CB8Ta$~7&s0d z04IzXpjt6dw;Qck>}n^ZBv`ffIVX;7L9tnw&*G53;)w zNRHu1rCx>kRcvwfBk7Lp4twbbC}T8b^QKJpRF~;P@KIzw1{66@0EPDz@LUsK;P@iA z2Cjn}fTA@#IDenxht$n2^!Ut|1r3Qz>@Q`%kUE0P8GwR+iJ&i)EJVxvgEAjP16)7= zhz9AP5R?(EDr-@C5hV>Eqft-fIGR{}A`#1UauaiTcE5yt!5X5uEyS?<$ay?KZsKub z>9>fk{zYo0aUW78524J#xB!$KguV`nK31S{KFXiOGrtmJ{>Z$8?}%f*Vh@Vj?1k|; z(abIM_yj#ZMvo7PBySS+y^kL65M#bYWUagx4?3X8jl7l!2 zl=%>99exor%4A2b#f>~mAhM#7kp};Kcy}Q0QAIwYEBS%m@E$;VYAC!%QAHCd+cfe8 z^W+i{>J!9P7l_i&;;fJI?KB)7CcE+w5|#XiG6!QnP}YMq>+mae@(s8JZZhxS4W4no z%)EoE5{is?_-Deq9hU7#KB5Yj> zTK3{Ndnnm1Br0o9l(`tnei%wFLeb$T@K2z)k>Xm%JYogjv<#^V zl=&A6zz@j$1iS_mcfSZ8Ay22w!8nR74ifk8<9YvX>SPCT?pE@2o6%z*N!F2{DoQ7vjMXoaULct{PDKcLHO0!d% z{7FE|rEKPOiFR;s!Ct<|2}MQ>TvO0Ln_NUYEM9^amLsDJGP)z9FER!rV-$IU$!Mai zN?A>wU>9djP+QLsyMIQW;7`0+*MMOhm2HNeraMvg06At(+=qO+49ZCt&%QZJiTf9j ziBQxkPrcfsn$d8So&lpK=YB+Oa z>d|8;dJIO7fwbEJ=uw9r{iv5d_-QYwdZ1}HxYT0F8cJ79eRQTis__0Q@tpggb z*rncvdzz^=GPTHaMV1dzf{?==l(gAO^r%3OGW6(#ln$IJG3qA8M*XF|@kl8&W(|hP zGRZgDF1e-;NRH_WLx|K$UlHn$UVrdZCeDlda#hsGlD2TUHmZY)8CP?oyJ6} zE!qFU%t6Y`+@zzquauexONn`;6j{VedkfZJSmsE9WufF-c9LAHYRR$U{SVgll4(6z z(ruPVn$0#zu{kcuw#@jlWd)w*S3nK|Jb=u-+}1Y5F|+`4W3fRbV~P-lgxIGyHmRY$ z%IwXh*j6KjwvJL@%ex99aM0kR|aBg%aynE-{YXB-(MHL^_R= z2&efH=DbNlnG+M@a!G<+K9&&I-z1b=OsEU7Kk~Mx;)@J37G+Wo9LMo}DCh6a0b!l1 z4>6~knWT`5Npy0M1ZOvibM}!~=U|C;j+7|o#zeZLON48_gp-R2V_r)@5$2vaDZ}eMRvnlpj?KUIrw50!}T=Am)!EZ#4x@H z#|HRFx~~y=9czhpx0gtFXNhq4lrVRH3H1n*5RVuM_Dqo=&s+&47Zc#sMf`b}lAq5= z@$s20-ahNZ)Ayiw_+AiqzxTz%?Wvn8R8naMqC09h;z_6aSD2e#7N$g^CL=#sEYQ;8wpjgLG6s!29 zVwtd0%o830FM``*5&wr+#xtjr@~T}?8@T(Z5FqT37a%9fyO6b*{HqnDN!Le5@`3KIIRN@Qhl7OyZ*ujS-7?<1Nw06LWNmTBD8y@R}q*EfmfjXgZr*2>< zjx__Uh3R2%iG0i_;FpFx%!+>a0&=HArtF6@Y&ibNv1$lvb3P4SqM->uA9sqRrLI`A zDkj}bP}GWChK2@&Eo!kv4YsJk7Bw*-1JK}VP`n04)lDd0A+cis@@8^bc`wh1$&hiZoJf^Tpe=xK zuKA#s(-?9<(3%3mnnsK?rbDx6oRc!bjw5=oPl^l=1lVFYjc8a0K!pyYQ1uIFbUVo3 zJVIgL;>P?VApd~Omh(_*X3{r<3=j?cs7B}M_#iL^3@Gnn8lR_OkEz&WD)yL)9#frw zCkO$F7`Om*0fTXR7*5(mM|cwFdx>1mcQ^oZFlKRjF6zz0Ec2-YRLx#UK6DX209=3u zm;eUUbf^H6HbD7&2c~?+KbD}!QuJ8r4I)4~=jddX;82S(>|%Uk(P0KlmvO`|8MLx` z0eQpW-5b8;%gMp4q%T-aJ6uCQ05ofv1G5fU>%j&Y0gW=`675Up(2SfD{9*{o) z`@ntRV2h*j`4FIJ4S~r~j`w4eMFP5BTb0vCMbvI__y zZ;?R|SqZSkl|^7bxB&hkwUo7la;9@Cg7PTuMAjUG&ja8gpvY5XV7i7t@JWu(fTzJ_ za0NUEUH}xUAz;11@eS}U_yBu+=+316D1yoY9_;rdu%_YNqVS(muuqjtM9UnAG9Sbm zI07FK4wUsUxkP8BM61eLly2lB`VglMBu6le`2u5zSSFI2m_cr09+AVchNt_th^{}O zaz3H}H&O0A`I_`h<31cOa8%ZTD0^Tic>sMK6n&Ijj6HKr+{v{BAuAR+>F{p{?~cT$ zo#ESEK0uFupvQYeTyN81yn!CCp~ox4Ro973ui?Be;6Tq2!8X1V!bN#=%mm`y@yz-dhupDE7|l_U|2&|jHZ(=0i`fUZ zVT%p;#cJBfa@HsOwAg2sDUC0mgKu#U}hoOx!Gh`kfvzD{_@Z!^W(96``_n>hX zWaaoIUC1eJGl%TSzi?bn*R~9O=A#2Magl3-Of6+{MUD@0f{+o3jCdral8eYfP62X? z@rp7mQH`8l=i9gdqX_#bEriKY7zWw3D8+*Y3=X>53lA>_=iMaBL$K-*lj&!+Y=sgDf2E4`s* zorC&aS~_bmkgwplKexnSY!FHvfH{1g#JQ1P3~{*K^u{JNwnkE6ZY>>+?UCnZ=ex$<~@nipG{) z4*3ULSBbOrmRL=o#Aw1LN)s!Q?4KK<$&oO-VhJS|6KvO8g6xM&p#2Q-*RB>n=EV4D z9}{owYvSYZJz(wu*oXX0N%&$ad7w0gd>lvdeIVzX=YW(_H+c*Z+4g}vizWFSJBidf zO1Rct!nD2;!rYi(E$@GD;QbE{y#Ik*jK5=r_&W9!AE$cpa+)fhPAkOSd9S!RKO(Nq zuZXM5cj874#@(6NA9)*)y)=_YaajyE!H9H*?a9;wdQ?z1Mfgch0MEJb*BEj!p-#3E zKgqyf6Yvqz4C3!^E}lNN;^xi% z3*Ii`?BfLj#mOf^9LdEv_+^XMuUNGH)ne!0Uo-*Z#5Q1&*aU79>%bFY75FS*|Aj#I zUkD)fN0zc4X+bgmSjyvBFqF@|(WeW#b-+gX)J-~d6NgR0u!(P&2{{~VaSX5%hX5zh z26_O0u@B@uoI!D-3CNdqyT?J&op#lTVB@rF!vmrI7M2BeYi$6VGg1RcNN=k zAF;tm*1TWaDk4oRBHM|1WQCYU^%j$;kzy1zhXe+9BKE40s3*b4;3q1Vu>@sS)+5cS zq)l|DO@M(_goEYu3+PpXe!1w9l+Tzqn|i<|ZrH>j!Axx9Y{e>8D;BXXVjkxuW^qAc z5*H=L@i=h#k!@x|i4jktEMerf`5#uta6#0vhIje^Lu`6u?45%gMs-{hF zz6f11J3v}Q?2FC3v57M_vCA+Mt27&OEcRlQ>O@Y3P9&8^oQl(=;y`IRfFGuH14F?y zu!d^~!Qv}{Kuxi zR7~@&sVHtLtPhf0S3t*_8wzODxj1w##JLnMcMzCN178Ua(Ak{_{~!m04&-5$qvrhH z_+vl(u|I7BC`(6b`ce<*lF4;aTM~jUUg%32SZvY9yrT&YYegrnflv#a36?!UAczKO z0A-6igE|a40W2e!+6ℑ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 literal 0 HcmV?d00001 From 59c0793b803e641c4c7d331a24bd41be6e728a38 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Jun 2017 12:37:25 +1000 Subject: [PATCH 19/43] Can now decode that bad progressive image --- .../Jpeg/Port/Components/ScanDecoder.cs | 162 +++++++++++++++--- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 16 +- 2 files changed, 138 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index da7bb52a9..73e597ccd 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -35,6 +35,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private int successiveACNextValue; + private bool endOfStreamReached; + /// /// Decodes the spectral scan /// @@ -50,8 +52,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// The spectral selection end /// The successive approximation bit high end /// The successive approximation bit low end - /// The representing the processed length in bytes - public long DecodeScan( + public void DecodeScan( Frame frame, Stream stream, HuffmanTables dcHuffmanTables, @@ -69,9 +70,10 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.specStart = spectralStart; this.specEnd = spectralEnd; this.successiveState = successive; + this.endOfStreamReached = false; + bool progressive = frame.Progressive; int mcusPerLine = frame.McusPerLine; - long startPosition = stream.Position; int mcu = 0; int mcuExpected; @@ -129,7 +131,6 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // Find marker this.bitsCount = 0; - long position = stream.Position; fileMarker = JpegDecoderCore.FindNextFileMarkerNew(stream); // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past @@ -137,20 +138,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (fileMarker.Invalid) { #if DEBUG - Debug.WriteLine($"DecodeScan - Unexpected MCU data at {stream.Position}, next marker is: {fileMarker.Marker}"); - + Debug.WriteLine($"DecodeScan - Unexpected MCU data at {stream.Position}, next marker is: {fileMarker.Marker:X}"); #endif - // stream.Position = fileMarker.Position; } ushort marker = fileMarker.Marker; - // if (marker <= 0xFF00) - // { - // throw new ImageFormatException("Marker was not found"); - // } - - // RSTn We've alread read the bytes and altered the position so no need to skip + // RSTn - We've alread read the bytes and altered the position so no need to skip if (marker >= JpegConstants.Markers.RST0 && marker <= JpegConstants.Markers.RST7) { continue; @@ -159,13 +153,10 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (!fileMarker.Invalid) { // We've found a valid marker. - // Rewind the stream to the position of the marker and beak + // Rewind the stream to the position of the marker and break stream.Position = fileMarker.Position; break; } - - // Rewind the stream - stream.Position = position; } fileMarker = JpegDecoderCore.FindNextFileMarkerNew(stream); @@ -175,12 +166,15 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (fileMarker.Invalid) { #if DEBUG - Debug.WriteLine($"DecodeScan - Unexpected MCU data, next marker is: {fileMarker.Marker}"); + Debug.WriteLine($"DecodeScan - Unexpected MCU data at {stream.Position}, next marker is: {fileMarker.Marker:X}"); #endif + } + else + { + // We've found a valid marker. + // Rewind the stream to the position of the marker stream.Position = fileMarker.Position; } - - return stream.Position - startPosition; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -205,6 +199,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeBlockBaseline(dcHuffmanTables, acHuffmanTables, ref component, mcu, stream); mcu++; } @@ -222,6 +221,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeMcuBaseline(dcHuffmanTables, acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); } } @@ -247,6 +251,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeBlockDCFirst(dcHuffmanTables, ref component, mcu, stream); mcu++; } @@ -264,6 +273,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeMcuDCFirst(dcHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); } } @@ -288,6 +302,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeBlockDCSuccessive(ref component, mcu, stream); mcu++; } @@ -305,6 +324,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeMcuDCSuccessive(ref component, mcusPerLine, mcu, j, k, stream); } } @@ -330,6 +354,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeBlockACFirst(acHuffmanTables, ref component, mcu, stream); mcu++; } @@ -347,6 +376,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeMcuACFirst(acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); } } @@ -372,6 +406,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeBlockACSuccessive(acHuffmanTables, ref component, mcu, stream); mcu++; } @@ -389,6 +428,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { + if (this.endOfStreamReached) + { + continue; + } + this.DecodeMcuACSuccessive(acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); } } @@ -509,6 +553,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } this.bitsData = stream.ReadByte(); + + if (this.bitsData == -0x1) + { + // We've encountered the end of the file stream which means there's no EOI marker in the image + this.endOfStreamReached = true; + } + if (this.bitsData == 0xFF) { int nextByte = stream.ReadByte(); @@ -527,10 +578,16 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private short DecodeHuffman(HuffmanBranch[] tree, Stream stream) { + // TODO: This is our bottleneck. We should use a faster algorithm with a LUT. HuffmanBranch[] node = tree; while (true) { int index = this.ReadBit(stream); + if (this.endOfStreamReached) + { + return -1; + } + HuffmanBranch branch = node[index]; if (branch.Value > -1) @@ -548,7 +605,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int n = 0; while (length > 0) { - n = (n << 1) | this.ReadBit(stream); + int bit = this.ReadBit(stream); + if (this.endOfStreamReached) + { + return -1; + } + + n = (n << 1) | bit; length--; } @@ -576,6 +639,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private void DecodeBaseline(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) { int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); + if (this.endOfStreamReached) + { + return; + } + int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream); component.BlockData[offset] = (short)(component.Pred += diff); @@ -583,6 +651,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components while (k < 64) { int rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + if (this.endOfStreamReached) + { + return; + } + int s = rs & 15; int r = rs >> 4; @@ -609,6 +682,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private void DecodeDCFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, Stream stream) { int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); + if (this.endOfStreamReached) + { + return; + } + int diff = t == 0 ? 0 : this.ReceiveAndExtend(t, stream) << this.successiveState; component.BlockData[offset] = (short)(component.Pred += diff); } @@ -616,7 +694,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeDCSuccessive(ref FrameComponent component, int offset, Stream stream) { - component.BlockData[offset] |= (short)(this.ReadBit(stream) << this.successiveState); + int bit = this.ReadBit(stream); + if (this.endOfStreamReached) + { + return; + } + + component.BlockData[offset] |= (short)(bit << this.successiveState); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -633,6 +717,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components while (k <= e) { short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + if (this.endOfStreamReached) + { + return; + } + int s = rs & 15; int r = rs >> 4; @@ -668,6 +757,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { case 0: // Initial state short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + if (this.endOfStreamReached) + { + return; + } + int s = rs & 15; r = rs >> 4; if (s == 0) @@ -699,7 +793,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components case 2: if (component.BlockData[offset + z] != 0) { - component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + int bit = this.ReadBit(stream); + if (this.endOfStreamReached) + { + return; + } + + component.BlockData[offset + z] += (short)(bit << this.successiveState); } else { @@ -714,7 +814,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components case 3: // Set value for a zero item if (component.BlockData[offset + z] != 0) { - component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + int bit = this.ReadBit(stream); + if (this.endOfStreamReached) + { + return; + } + + component.BlockData[offset + z] += (short)(bit << this.successiveState); } else { @@ -726,7 +832,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components case 4: // Eob if (component.BlockData[offset + z] != 0) { - component.BlockData[offset + z] += (short)(this.ReadBit(stream) << this.successiveState); + int bit = this.ReadBit(stream); + if (this.endOfStreamReached) + { + return; + } + + component.BlockData[offset + z] += (short)(bit << this.successiveState); } break; diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 2c2cd57a5..152a2b43a 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -720,7 +720,7 @@ namespace ImageSharp.Formats.Jpeg.Port int successiveApproximation = this.temp[2]; var scanDecoder = default(ScanDecoder); - long position = scanDecoder.DecodeScan( + scanDecoder.DecodeScan( this.frame, this.InputStream, this.dcHuffmanTables, @@ -733,20 +733,6 @@ namespace ImageSharp.Formats.Jpeg.Port spectralEnd, successiveApproximation >> 4, successiveApproximation & 15); - - this.InputStream.Position += position; - - Debug.WriteLine("spectralStart= " + spectralStart); - Debug.WriteLine("spectralEnd= " + spectralEnd); - Debug.WriteLine("successiveApproximation= " + successiveApproximation); - Debug.WriteLine("Components after"); - //for (int i = 0; i < 3; i++) - //{ - // for (int j = 0; j < 10; j++) - // { - // Debug.WriteLine("component [" + i + "] : value [" + j + "] =" + this.frame.Components[i].BlockData[j] + "]"); - // } - //} } /// From 827ca83bf052c471df35456d97d4c19b4b07792e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Jun 2017 14:35:46 +1000 Subject: [PATCH 20/43] Now decodes all images --- .../Formats/Jpeg/Port/Components/JFif.cs | 40 ++- .../Jpeg/Port/Components/ScanDecoder.cs | 7 +- .../Formats/Jpeg/Port/JpegConstants.cs | 94 ++++++- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 251 ++++++++++-------- tests/ImageSharp.Tests/TestFile.cs | 20 +- 5 files changed, 291 insertions(+), 121 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs index 7fa6c44d0..6baecdf68 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs @@ -5,10 +5,13 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { + using System; + /// /// Provides information about the JFIF marker segment + /// TODO: Thumbnail? /// - internal struct JFif + internal struct JFif : IEquatable { /// /// The major version @@ -38,6 +41,39 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// public short YDensity; - // TODO: Thumbnail? + /// + public bool Equals(JFif other) + { + return this.MajorVersion == other.MajorVersion + && this.MinorVersion == other.MinorVersion + && this.DensityUnits == other.DensityUnits + && this.XDensity == other.XDensity + && this.YDensity == other.YDensity; + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is JFif && this.Equals((JFif)obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = this.MajorVersion.GetHashCode(); + hashCode = (hashCode * 397) ^ this.MinorVersion.GetHashCode(); + hashCode = (hashCode * 397) ^ this.DensityUnits.GetHashCode(); + hashCode = (hashCode * 397) ^ this.XDensity.GetHashCode(); + hashCode = (hashCode * 397) ^ this.YDensity.GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 73e597ccd..4ec7571b5 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -17,6 +17,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// internal struct ScanDecoder { + private byte[] markerBuffer; + private int bitsData; private int bitsCount; @@ -66,6 +68,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int successivePrev, int successive) { + this.markerBuffer = new byte[2]; this.compIndex = componentIndex; this.specStart = spectralStart; this.specEnd = spectralEnd; @@ -131,7 +134,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // Find marker this.bitsCount = 0; - fileMarker = JpegDecoderCore.FindNextFileMarkerNew(stream); + fileMarker = JpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream); // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past // those to attempt to find a valid marker (fixes issue4090.pdf) in original code. @@ -159,7 +162,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } } - fileMarker = JpegDecoderCore.FindNextFileMarkerNew(stream); + fileMarker = JpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream); // Some images include more Scan blocks than expected, skip past those and // attempt to find the next valid marker (fixes issue8182.pdf) in original code. diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs index 08ae5543d..a02e05591 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs @@ -196,11 +196,6 @@ namespace ImageSharp.Formats.Jpeg.Port /// public const ushort RST7 = 0xFFD7; - /// - /// Marker prefix. Next byte is a marker. - /// - public const ushort XFF = 0xFFFF; - /// /// Contains JFIF specific markers /// @@ -224,7 +219,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Represents the null "0" marker /// - public const byte Null = 0; + public const byte Null = 0x0; } /// @@ -272,6 +267,93 @@ namespace ImageSharp.Formats.Jpeg.Port /// public const byte ColorTransformYcck = 2; } + + /// + /// Contains EXIF specific markers + /// + public static class Exif + { + /// + /// Represents E in ASCII + /// + public const byte E = 0x45; + + /// + /// Represents x in ASCII + /// + public const byte X = 0x78; + + /// + /// Represents i in ASCII + /// + public const byte I = 0x69; + + /// + /// Represents f in ASCII + /// + public const byte F = 0x66; + + /// + /// Represents the null "0" marker + /// + public const byte Null = 0x0; + } + + /// + /// Contains ICC specific markers + /// + public static class ICC + { + /// + /// Represents I in ASCII + /// + public const byte I = 0x49; + + /// + /// Represents C in ASCII + /// + public const byte C = 0x43; + + /// + /// Represents _ in ASCII + /// + public const byte UnderScore = 0x5F; + + /// + /// Represents P in ASCII + /// + public const byte P = 0x50; + + /// + /// Represents R in ASCII + /// + public const byte R = 0x52; + + /// + /// Represents O in ASCII + /// + public const byte O = 0x4F; + + /// + /// Represents F in ASCII + /// + public const byte F = 0x46; + + /// + /// Represents L in ASCII + /// + public const byte L = 0x4C; + + /// + /// Represents E in ASCII + /// + public const byte E = 0x45; + + /// + /// Represents the null "0" marker + /// + public const byte Null = 0x0; + } } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 152a2b43a..6a1d6311c 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -7,7 +7,6 @@ namespace ImageSharp.Formats.Jpeg.Port { using System; using System.Collections.Generic; - using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; @@ -18,7 +17,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Performs the jpeg decoding operation. - /// Ported from + /// Ported from with additional fixes to handle common encoding errors /// internal sealed class JpegDecoderCore : IDisposable { @@ -37,7 +36,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// private readonly byte[] temp = new byte[2 * 16 * 4]; - private readonly byte[] uint16Buffer = new byte[2]; + private readonly byte[] markerBuffer = new byte[2]; private QuantizationTables quantizationTables; @@ -57,7 +56,12 @@ namespace ImageSharp.Formats.Jpeg.Port private int imageHeight; - private int numComponents; + private int numberOfComponents; + + /// + /// Whether the image has a EXIF header + /// + private bool isExif; /// /// Contains information about the JFIF marker @@ -94,14 +98,13 @@ namespace ImageSharp.Formats.Jpeg.Port public Stream InputStream { get; private set; } /// - /// Finds the next file marker within the byte stream. Not used but I'm keeping it for now for testing + /// Finds the next file marker within the byte stream. /// + /// The buffer to read file markers to /// The input stream /// The - public static FileMarker FindNextFileMarkerNew(Stream stream) + public static FileMarker FindNextFileMarker(byte[] marker, Stream stream) { - byte[] marker = new byte[2]; - int value = stream.Read(marker, 0, 2); if (value == 0) @@ -131,60 +134,7 @@ namespace ImageSharp.Formats.Jpeg.Port } /// - /// Finds the next file marker within the byte stream - /// - /// The input stream - /// The - public static FileMarker FindNextFileMarker(Stream stream) - { - byte[] buffer = new byte[2]; - long maxPos = stream.Length - 1; - long currentPos = stream.Position; - long newPos = currentPos; - - if (currentPos >= maxPos) - { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } - - int value = stream.Read(buffer, 0, 2); - - if (value == 0) - { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } - - ushort currentMarker = (ushort)((buffer[0] << 8) | buffer[1]); - if (currentMarker >= JpegConstants.Markers.SOF0 && currentMarker <= JpegConstants.Markers.COM) - { - return new FileMarker(currentMarker, stream.Position - 2); - } - - value = stream.Read(buffer, 0, 2); - - if (value == 0) - { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } - - ushort newMarker = (ushort)((buffer[0] << 8) | buffer[1]); - while (!(newMarker >= JpegConstants.Markers.SOF0 && newMarker <= JpegConstants.Markers.COM)) - { - if (++newPos >= maxPos) - { - return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true); - } - - stream.Read(buffer, 0, 2); - newMarker = (ushort)((buffer[0] << 8) | buffer[1]); - } - - return new FileMarker(newMarker, newPos, true); - } - - /// - /// Decodes the image from the specified and sets - /// the data to image. + /// Decodes the image from the specified and sets the data to image. /// /// The pixel format. /// The stream, where the image should be. @@ -193,10 +143,13 @@ namespace ImageSharp.Formats.Jpeg.Port where TPixel : struct, IPixel { this.InputStream = stream; - this.ParseStream(); - var image = new Image(this.imageWidth, this.imageHeight); - this.GetData(image); + var metadata = new ImageMetaData(); + this.ParseStream(metadata, false); + + var image = new Image(this.configuration, this.imageWidth, this.imageHeight, metadata); + this.FillPixelData(image); + this.AssignResolution(image); return image; } @@ -223,8 +176,11 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Parses the input stream for file markers /// - private void ParseStream() + /// Contains the metadata for an image + /// Whether to decode metadata only. + private void ParseStream(ImageMetaData metaData, bool metadataOnly) { + // TODO: metadata only logic // Check for the Start Of Image marker. var fileMarker = new FileMarker(this.ReadUint16(), 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) @@ -251,7 +207,12 @@ namespace ImageSharp.Formats.Jpeg.Port break; case JpegConstants.Markers.APP1: + this.ProcessApp1Marker(remaining, metaData); + break; + case JpegConstants.Markers.APP2: + this.ProcessApp2Marker(remaining, metaData); + break; case JpegConstants.Markers.APP3: case JpegConstants.Markers.APP4: case JpegConstants.Markers.APP5: @@ -298,36 +259,10 @@ namespace ImageSharp.Formats.Jpeg.Port case JpegConstants.Markers.SOS: this.ProcessStartOfScanMarker(); break; - - case JpegConstants.Markers.XFF: - if ((byte)this.InputStream.ReadByte() != 0xFF) - { - // Avoid skipping a valid marker - this.InputStream.Position -= 1; - } - - break; - - //default: - - // // TODO: Not convinced this is required - // // Skip back as it could be incorrect encoding -- last 0xFF byte of the previous - // // block was eaten by the encoder - // this.InputStream.Position -= 3; - // this.InputStream.Read(this.temp, 0, 2); - // if (this.temp[0] == 0xFF && this.temp[1] >= 0xC0 && this.temp[1] <= 0xFE) - // { - // // Rewind that last bytes we read - // this.InputStream.Position -= 2; - // break; - // } - - // // throw new ImageFormatException($"Unknown Marker {fileMarker.Marker} at {fileMarker.Position}"); - // break; } - // Read on. TODO: Test this on damaged images. - fileMarker = FindNextFileMarkerNew(this.InputStream); + // Read on. + fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream); } this.imageWidth = this.frame.SamplesPerLine; @@ -349,7 +284,7 @@ namespace ImageSharp.Formats.Jpeg.Port this.components.Components[i] = component; } - this.numComponents = this.components.Components.Length; + this.numberOfComponents = this.components.Components.Length; } /// @@ -357,24 +292,24 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// The pixel format. /// The image - private void GetData(Image image) + private void FillPixelData(Image image) where TPixel : struct, IPixel { - if (this.numComponents > 4) + if (this.numberOfComponents > 4) { - throw new ImageFormatException($"Unsupported color mode. Max components 4; found {this.numComponents}"); + throw new ImageFormatException($"Unsupported color mode. Max components 4; found {this.numberOfComponents}"); } - this.pixelArea = new JpegPixelArea(image.Width, image.Height, this.numComponents); + this.pixelArea = new JpegPixelArea(image.Width, image.Height, this.numberOfComponents); this.pixelArea.LinearizeBlockData(this.components, image.Width, image.Height); - if (this.numComponents == 1) + if (this.numberOfComponents == 1) { this.FillGrayScaleImage(image); return; } - if (this.numComponents == 3) + if (this.numberOfComponents == 3) { if (this.adobe.Equals(default(Adobe)) || this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYCbCr) { @@ -386,7 +321,7 @@ namespace ImageSharp.Formats.Jpeg.Port } } - if (this.numComponents == 4) + if (this.numberOfComponents == 4) { if (this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYcck) { @@ -399,6 +334,34 @@ namespace ImageSharp.Formats.Jpeg.Port } } + /// + /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header or EXIF metadata. + /// + /// The pixel format. + /// The image to assign the resolution to. + private void AssignResolution(Image image) + where TPixel : struct, IPixel + { + if (this.isExif) + { + ExifValue horizontal = image.MetaData.ExifProfile.GetValue(ExifTag.XResolution); + ExifValue vertical = image.MetaData.ExifProfile.GetValue(ExifTag.YResolution); + double horizontalValue = horizontal != null ? ((Rational)horizontal.Value).ToDouble() : 0; + double verticalValue = vertical != null ? ((Rational)vertical.Value).ToDouble() : 0; + + if (horizontalValue > 0 && verticalValue > 0) + { + image.MetaData.HorizontalResolution = horizontalValue; + image.MetaData.VerticalResolution = verticalValue; + } + } + else if (this.jFif.XDensity > 0 && this.jFif.YDensity > 0) + { + image.MetaData.HorizontalResolution = this.jFif.XDensity; + image.MetaData.VerticalResolution = this.jFif.YDensity; + } + } + /// /// Processes the application header containing the JFIF identifier plus extra data. /// @@ -440,6 +403,86 @@ namespace ImageSharp.Formats.Jpeg.Port } } + /// + /// Processes the App1 marker retrieving any stored metadata + /// + /// The remaining bytes in the segment block. + /// The image. + private void ProcessApp1Marker(int remaining, ImageMetaData metadata) + { + if (remaining < 6 || this.options.IgnoreMetadata) + { + // Skip the application header length + this.InputStream.Skip(remaining); + return; + } + + byte[] profile = new byte[remaining]; + this.InputStream.Read(profile, 0, remaining); + + if (profile[0] == JpegConstants.Markers.Exif.E && + profile[1] == JpegConstants.Markers.Exif.X && + profile[2] == JpegConstants.Markers.Exif.I && + profile[3] == JpegConstants.Markers.Exif.F && + profile[4] == JpegConstants.Markers.Exif.Null && + profile[5] == JpegConstants.Markers.Exif.Null) + { + this.isExif = true; + metadata.ExifProfile = new ExifProfile(profile); + } + } + + /// + /// Processes the App2 marker retrieving any stored ICC profile information + /// + /// The remaining bytes in the segment block. + /// The image. + private void ProcessApp2Marker(int remaining, ImageMetaData metadata) + { + // Length is 14 though we only need to check 12. + const int Icclength = 14; + if (remaining < Icclength || this.options.IgnoreMetadata) + { + this.InputStream.Skip(remaining); + return; + } + + byte[] identifier = new byte[Icclength]; + this.InputStream.Read(identifier, 0, Icclength); + remaining -= Icclength; // We have read it by this point + + if (identifier[0] == JpegConstants.Markers.ICC.I && + identifier[1] == JpegConstants.Markers.ICC.C && + identifier[2] == JpegConstants.Markers.ICC.C && + identifier[3] == JpegConstants.Markers.ICC.UnderScore && + identifier[4] == JpegConstants.Markers.ICC.P && + identifier[5] == JpegConstants.Markers.ICC.R && + identifier[6] == JpegConstants.Markers.ICC.O && + identifier[7] == JpegConstants.Markers.ICC.F && + identifier[8] == JpegConstants.Markers.ICC.I && + identifier[9] == JpegConstants.Markers.ICC.L && + identifier[10] == JpegConstants.Markers.ICC.E && + identifier[11] == JpegConstants.Markers.ICC.Null) + { + byte[] profile = new byte[remaining]; + this.InputStream.Read(profile, 0, remaining); + + if (metadata.IccProfile == null) + { + metadata.IccProfile = new IccProfile(profile); + } + else + { + metadata.IccProfile.Extend(profile); + } + } + else + { + // Not an ICC profile we can handle. Skip the remaining bytes so we can carry on and ignore this. + this.InputStream.Skip(remaining); + } + } + /// /// Processes the application header containing the Adobe identifier /// which stores image encoding information for DCT filters. @@ -951,8 +994,8 @@ namespace ImageSharp.Formats.Jpeg.Port [MethodImpl(MethodImplOptions.AggressiveInlining)] private ushort ReadUint16() { - this.InputStream.Read(this.uint16Buffer, 0, 2); - return (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); + this.InputStream.Read(this.markerBuffer, 0, 2); + return (ushort)((this.markerBuffer[0] << 8) | this.markerBuffer[1]); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestFile.cs b/tests/ImageSharp.Tests/TestFile.cs index d274d61a2..bcfb6cb13 100644 --- a/tests/ImageSharp.Tests/TestFile.cs +++ b/tests/ImageSharp.Tests/TestFile.cs @@ -27,6 +27,8 @@ namespace ImageSharp.Tests /// private static readonly string FormatsDirectory = GetFormatsDirectory(); + private static readonly object Locker = new object(); + /// /// The image. /// @@ -142,7 +144,10 @@ namespace ImageSharp.Tests private Image GetImage() { - return this.image ?? (this.image = Image.Load(this.Bytes)); + lock (Locker) + { + return this.image ?? (this.image = Image.Load(this.Bytes)); + } } /// @@ -155,16 +160,17 @@ namespace ImageSharp.Tests { var directories = new List { "TestImages/Formats/", // Here for code coverage tests. - "tests/ImageSharp.Tests/TestImages/Formats/", // from travis/build script - "../../../../../ImageSharp.Tests/TestImages/Formats/", // from Sandbox46 + "tests/ImageSharp.Tests/TestImages/Formats/", // From travis/build script + "../../../../../ImageSharp.Tests/TestImages/Formats/", // From Sandbox46 "../../../../TestImages/Formats/", "../../../TestImages/Formats/" }; - directories = directories.SelectMany(x => new[] - { - Path.GetFullPath(x) - }).ToList(); + directories = directories + .SelectMany(x => new[] + { + Path.GetFullPath(x) + }).ToList(); AddFormatsDirectoryFromTestAssebmlyPath(directories); From e2d26eb8104ff7688c2c4d3bff544587941f731b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Jun 2017 15:26:13 +1000 Subject: [PATCH 21/43] Fix #159 --- .../Jpeg/Port/Components/ScanDecoder.cs | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 4ec7571b5..13a63c5c3 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -39,6 +39,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private bool endOfStreamReached; + private bool unexpectedMarkerReached; + /// /// Decodes the spectral scan /// @@ -74,6 +76,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.specEnd = spectralEnd; this.successiveState = successive; this.endOfStreamReached = false; + this.unexpectedMarkerReached = false; bool progressive = frame.Progressive; int mcusPerLine = frame.McusPerLine; @@ -202,7 +205,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -224,7 +227,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -254,7 +257,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -276,7 +279,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -305,7 +308,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -327,7 +330,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -357,7 +360,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -379,7 +382,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -409,7 +412,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components ref FrameComponent component = ref components[this.compIndex]; for (int n = 0; n < mcuToRead; n++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -431,7 +434,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { for (int k = 0; k < h; k++) { - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { continue; } @@ -568,7 +571,12 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int nextByte = stream.ReadByte(); if (nextByte != 0) { - throw new ImageFormatException($"Unexpected marker {(this.bitsData << 8) | nextByte}"); +#if DEBUG + Debug.WriteLine($"DecodeScan - Unexpected marker {(this.bitsData << 8) | nextByte:X} at {stream.Position}"); +#endif + // We've encountered an unexpected marker. Reverse the stream and exit. + this.unexpectedMarkerReached = true; + stream.Position -= 2; } // Unstuff 0 @@ -586,7 +594,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components while (true) { int index = this.ReadBit(stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return -1; } @@ -609,7 +617,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components while (length > 0) { int bit = this.ReadBit(stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return -1; } @@ -642,7 +650,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private void DecodeBaseline(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) { int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } @@ -654,7 +662,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components while (k < 64) { int rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } @@ -685,7 +693,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private void DecodeDCFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, Stream stream) { int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } @@ -698,7 +706,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private void DecodeDCSuccessive(ref FrameComponent component, int offset, Stream stream) { int bit = this.ReadBit(stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } @@ -720,7 +728,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components while (k <= e) { short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } @@ -760,7 +768,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { case 0: // Initial state short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } @@ -797,7 +805,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (component.BlockData[offset + z] != 0) { int bit = this.ReadBit(stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } @@ -818,7 +826,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (component.BlockData[offset + z] != 0) { int bit = this.ReadBit(stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } @@ -836,7 +844,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (component.BlockData[offset + z] != 0) { int bit = this.ReadBit(stream); - if (this.endOfStreamReached) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; } From 76e91db9b2f2f3df3735db79c59122661c2f49a8 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Wed, 28 Jun 2017 11:30:59 +0100 Subject: [PATCH 22/43] use an offset span instead of buffer --- .../Formats/Jpeg/Port/Components/IDCT.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs index 0e5a97012..760844ae2 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -30,7 +30,7 @@ public static void QuantizeAndInverse(QuantizationTables quantizationTables, ref FrameComponent component, int blockBufferOffset, Buffer computationBuffer) { Span qt = quantizationTables.Tables.GetRowSpan(component.QuantizationIdentifier); - Buffer blockData = component.BlockData; + Span blockData = component.BlockData.Slice(blockBufferOffset); int v0, v1, v2, v3, v4, v5, v6, v7; int p0, p1, p2, p3, p4, p5, p6, p7; int t; @@ -39,14 +39,14 @@ for (int row = 0; row < 64; row += 8) { // gather block data - p0 = blockData[blockBufferOffset + row]; - p1 = blockData[blockBufferOffset + row + 1]; - p2 = blockData[blockBufferOffset + row + 2]; - p3 = blockData[blockBufferOffset + row + 3]; - p4 = blockData[blockBufferOffset + row + 4]; - p5 = blockData[blockBufferOffset + row + 5]; - p6 = blockData[blockBufferOffset + row + 6]; - p7 = blockData[blockBufferOffset + row + 7]; + p0 = blockData[row]; + p1 = blockData[row + 1]; + p2 = blockData[row + 2]; + p3 = blockData[row + 3]; + p4 = blockData[row + 4]; + p5 = blockData[row + 5]; + p6 = blockData[row + 6]; + p7 = blockData[row + 7]; // dequant p0 p0 *= qt[row]; @@ -141,14 +141,14 @@ t = (t < -2040) ? 0 : (t >= 2024) ? 255 : (t + 2056) >> 4; short st = (short)t; - blockData[blockBufferOffset + col] = st; - blockData[blockBufferOffset + col + 8] = st; - blockData[blockBufferOffset + col + 16] = st; - blockData[blockBufferOffset + col + 24] = st; - blockData[blockBufferOffset + col + 32] = st; - blockData[blockBufferOffset + col + 40] = st; - blockData[blockBufferOffset + col + 48] = st; - blockData[blockBufferOffset + col + 56] = st; + blockData[col] = st; + blockData[col + 8] = st; + blockData[col + 16] = st; + blockData[col + 24] = st; + blockData[col + 32] = st; + blockData[col + 40] = st; + blockData[col + 48] = st; + blockData[col + 56] = st; continue; } @@ -208,14 +208,14 @@ p7 = (p7 < 16) ? 0 : (p7 >= 4080) ? 255 : p7 >> 4; // store block data - blockData[blockBufferOffset + col] = (short)p0; - blockData[blockBufferOffset + col + 8] = (short)p1; - blockData[blockBufferOffset + col + 16] = (short)p2; - blockData[blockBufferOffset + col + 24] = (short)p3; - blockData[blockBufferOffset + col + 32] = (short)p4; - blockData[blockBufferOffset + col + 40] = (short)p5; - blockData[blockBufferOffset + col + 48] = (short)p6; - blockData[blockBufferOffset + col + 56] = (short)p7; + blockData[col] = (short)p0; + blockData[col + 8] = (short)p1; + blockData[col + 16] = (short)p2; + blockData[col + 24] = (short)p3; + blockData[col + 32] = (short)p4; + blockData[col + 40] = (short)p5; + blockData[col + 48] = (short)p6; + blockData[col + 56] = (short)p7; } } } From db2b712e02cbf4b79e0f3dc98ed8044505ed12d1 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Wed, 28 Jun 2017 12:33:47 +0100 Subject: [PATCH 23/43] additional usages of Span --- .../Formats/Jpeg/Port/Components/IDCT.cs | 49 ++++++++++--------- .../Jpeg/Port/Components/JpegPixelArea.cs | 8 +-- .../Jpeg/Port/Components/ScanDecoder.cs | 18 ++++--- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs index 760844ae2..f931c3d6b 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -31,6 +31,7 @@ { Span qt = quantizationTables.Tables.GetRowSpan(component.QuantizationIdentifier); Span blockData = component.BlockData.Slice(blockBufferOffset); + Span computationBufferSpan = computationBuffer; int v0, v1, v2, v3, v4, v5, v6, v7; int p0, p1, p2, p3, p4, p5, p6, p7; int t; @@ -56,14 +57,14 @@ { t = ((DctSqrt2 * p0) + 512) >> 10; short st = (short)t; - computationBuffer[row] = st; - computationBuffer[row + 1] = st; - computationBuffer[row + 2] = st; - computationBuffer[row + 3] = st; - computationBuffer[row + 4] = st; - computationBuffer[row + 5] = st; - computationBuffer[row + 6] = st; - computationBuffer[row + 7] = st; + computationBufferSpan[row] = st; + computationBufferSpan[row + 1] = st; + computationBufferSpan[row + 2] = st; + computationBufferSpan[row + 3] = st; + computationBufferSpan[row + 4] = st; + computationBufferSpan[row + 5] = st; + computationBufferSpan[row + 6] = st; + computationBufferSpan[row + 7] = st; continue; } @@ -110,27 +111,27 @@ v6 = t; // stage 1 - computationBuffer[row] = (short)(v0 + v7); - computationBuffer[row + 7] = (short)(v0 - v7); - computationBuffer[row + 1] = (short)(v1 + v6); - computationBuffer[row + 6] = (short)(v1 - v6); - computationBuffer[row + 2] = (short)(v2 + v5); - computationBuffer[row + 5] = (short)(v2 - v5); - computationBuffer[row + 3] = (short)(v3 + v4); - computationBuffer[row + 4] = (short)(v3 - v4); + computationBufferSpan[row] = (short)(v0 + v7); + computationBufferSpan[row + 7] = (short)(v0 - v7); + computationBufferSpan[row + 1] = (short)(v1 + v6); + computationBufferSpan[row + 6] = (short)(v1 - v6); + computationBufferSpan[row + 2] = (short)(v2 + v5); + computationBufferSpan[row + 5] = (short)(v2 - v5); + computationBufferSpan[row + 3] = (short)(v3 + v4); + computationBufferSpan[row + 4] = (short)(v3 - v4); } // inverse DCT on columns for (int col = 0; col < 8; ++col) { - p0 = computationBuffer[col]; - p1 = computationBuffer[col + 8]; - p2 = computationBuffer[col + 16]; - p3 = computationBuffer[col + 24]; - p4 = computationBuffer[col + 32]; - p5 = computationBuffer[col + 40]; - p6 = computationBuffer[col + 48]; - p7 = computationBuffer[col + 56]; + p0 = computationBufferSpan[col]; + p1 = computationBufferSpan[col + 8]; + p2 = computationBufferSpan[col + 16]; + p3 = computationBufferSpan[col + 24]; + p4 = computationBufferSpan[col + 32]; + p5 = computationBufferSpan[col + 40]; + p6 = computationBufferSpan[col + 48]; + p7 = computationBufferSpan[col + 56]; // check for all-zero AC coefficients if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs index eafc6c33c..33cc44df4 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs @@ -72,17 +72,19 @@ namespace ImageSharp.Formats.Jpeg.Port.Components float scaleX = this.imageWidth / (float)width; float scaleY = this.imageHeight / (float)height; this.componentData = new Buffer(width * height * numberOfComponents); + Span componentDataSpan = this.componentData; const uint Mask3Lsb = 0xFFFFFFF8; // Used to clear the 3 LSBs using (var xScaleBlockOffset = new Buffer(width)) { + Span xScaleBlockOffsetSpan = xScaleBlockOffset; for (int i = 0; i < numberOfComponents; i++) { ref Component component = ref components.Components[i]; float componentScaleX = component.ScaleX * scaleX; float componentScaleY = component.ScaleY * scaleY; int offset = i; - Buffer output = component.Output; + Span output = component.Output; int blocksPerScanline = (component.BlocksPerLine + 1) << 3; // Precalculate the xScaleBlockOffset @@ -90,7 +92,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components for (int x = 0; x < width; x++) { j = 0 | (int)(x * componentScaleX); - xScaleBlockOffset[x] = (int)((j & Mask3Lsb) << 3) | (j & 7); + xScaleBlockOffsetSpan[x] = (int)((j & Mask3Lsb) << 3) | (j & 7); } // Linearize the blocks of the component @@ -100,7 +102,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int index = blocksPerScanline * (int)(j & Mask3Lsb) | ((j & 7) << 3); for (int x = 0; x < width; x++) { - this.componentData[offset] = (byte)output[index + xScaleBlockOffset[x]]; + componentDataSpan[offset] = (byte)output[index + xScaleBlockOffsetSpan[x]]; offset += numberOfComponents; } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 13a63c5c3..5a31fd89f 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -723,6 +723,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components return; } + var componentBlockDataSpan = component.BlockData.Span; int k = this.specStart; int e = this.specEnd; while (k <= e) @@ -750,7 +751,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components k += r; byte z = QuantizationTables.DctZigZag[k]; - component.BlockData[offset + z] = (short)(this.ReceiveAndExtend(s, stream) * (1 << this.successiveState)); + componentBlockDataSpan[offset + z] = (short)(this.ReceiveAndExtend(s, stream) * (1 << this.successiveState)); k++; } } @@ -761,6 +762,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int k = this.specStart; int e = this.specEnd; int r = 0; + var componentBlockDataSpan = component.BlockData.Span; while (k <= e) { byte z = QuantizationTables.DctZigZag[k]; @@ -802,7 +804,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; case 1: // Skipping r zero items case 2: - if (component.BlockData[offset + z] != 0) + if (componentBlockDataSpan[offset + z] != 0) { int bit = this.ReadBit(stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) @@ -810,7 +812,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components return; } - component.BlockData[offset + z] += (short)(bit << this.successiveState); + componentBlockDataSpan[offset + z] += (short)(bit << this.successiveState); } else { @@ -823,7 +825,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components break; case 3: // Set value for a zero item - if (component.BlockData[offset + z] != 0) + if (componentBlockDataSpan[offset + z] != 0) { int bit = this.ReadBit(stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) @@ -831,17 +833,17 @@ namespace ImageSharp.Formats.Jpeg.Port.Components return; } - component.BlockData[offset + z] += (short)(bit << this.successiveState); + componentBlockDataSpan[offset + z] += (short)(bit << this.successiveState); } else { - component.BlockData[offset + z] = (short)(this.successiveACNextValue << this.successiveState); + componentBlockDataSpan[offset + z] = (short)(this.successiveACNextValue << this.successiveState); this.successiveACState = 0; } break; case 4: // Eob - if (component.BlockData[offset + z] != 0) + if (componentBlockDataSpan[offset + z] != 0) { int bit = this.ReadBit(stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) @@ -849,7 +851,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components return; } - component.BlockData[offset + z] += (short)(bit << this.successiveState); + componentBlockDataSpan[offset + z] += (short)(bit << this.successiveState); } break; From 543924085ec7368be1270b66fb63128b76bfee12 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 28 Jun 2017 22:27:43 +0200 Subject: [PATCH 24/43] fixed Sandbox46 execution --- tests/ImageSharp.Sandbox46/Program.cs | 4 ++-- tests/ImageSharp.Tests/TestFile.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index 6f93df16e..23b48caec 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -39,10 +39,10 @@ namespace ImageSharp.Sandbox46 /// public static void Main(string[] args) { - // RunDecodeJpegProfilingTests(); + RunDecodeJpegProfilingTests(); // RunToVector4ProfilingTest(); - RunResizeProfilingTest(); + //RunResizeProfilingTest(); Console.ReadLine(); } diff --git a/tests/ImageSharp.Tests/TestFile.cs b/tests/ImageSharp.Tests/TestFile.cs index bcfb6cb13..698ad8313 100644 --- a/tests/ImageSharp.Tests/TestFile.cs +++ b/tests/ImageSharp.Tests/TestFile.cs @@ -162,6 +162,7 @@ namespace ImageSharp.Tests "TestImages/Formats/", // Here for code coverage tests. "tests/ImageSharp.Tests/TestImages/Formats/", // From travis/build script "../../../../../ImageSharp.Tests/TestImages/Formats/", // From Sandbox46 + "../ImageSharp.Tests/TestImages/Formats/", // From Sandbox46 Program.cs "../../../../TestImages/Formats/", "../../../TestImages/Formats/" }; From ea0abc934afa112cdb8417aff951ca0089ba971b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Jun 2017 09:12:29 +1000 Subject: [PATCH 25/43] Rough working better Huffman --- .../Jpeg/Port/Components/HuffmanTable.cs | 136 ++++++++++++++++++ .../Jpeg/Port/Components/HuffmanTables.cs | 4 +- .../Jpeg/Port/Components/ScanDecoder.cs | 37 +++-- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 63 ++------ 4 files changed, 170 insertions(+), 70 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs new file mode 100644 index 000000000..995fd550c --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs @@ -0,0 +1,136 @@ +namespace ImageSharp.Formats.Jpeg.Port.Components +{ + using System; + using System.Runtime.CompilerServices; + + /// + /// Represents a HUffman Table + /// + internal sealed class HuffmanTable + { + private short[] huffcode = new short[257]; + private short[] huffsize = new short[257]; + private short[] valOffset = new short[18]; + private long[] maxcode = new long[18]; + + private byte[] huffval; + private byte[] bits; + + /// + /// Initializes a new instance of the class. + /// + /// The code lengths + /// The huffman values + public HuffmanTable(byte[] lengths, byte[] values) + { + this.huffval = new byte[values.Length]; + Buffer.BlockCopy(values, 0, this.huffval, 0, values.Length); + this.bits = new byte[lengths.Length]; + Buffer.BlockCopy(lengths, 0, this.bits, 0, lengths.Length); + + this.GenerateSizeTable(); + this.GenerateCodeTable(); + this.GenerateDecoderTables(); + } + + /// + /// Gets the Huffman value code at the given index + /// + /// The index + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short GetHuffVal(int i) + { + return this.huffval[i]; + } + + /// + /// Gets the max code at the given index + /// + /// The index + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetMaxCode(int i) + { + return this.maxcode[i]; + } + + /// + /// Gets the index to the locatation of the huffman value + /// + /// The index + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetValPtr(int i) + { + return this.valOffset[i]; + } + + /// + /// Figure C.1: make table of Huffman code length for each symbol + /// + private void GenerateSizeTable() + { + short index = 0; + for (short l = 1; l <= 16; l++) + { + byte i = this.bits[l]; + for (short j = 0; j < i; j++) + { + this.huffsize[index] = l; + index++; + } + } + + this.huffsize[index] = 0; + } + + /// + /// Figure C.2: generate the codes themselves + /// + private void GenerateCodeTable() + { + short k = 0; + short si = this.huffsize[0]; + short code = 0; + for (short i = 0; i < this.huffsize.Length; i++) + { + while (this.huffsize[k] == si) + { + this.huffcode[k] = code; + code++; + k++; + } + + code <<= 1; + si++; + } + } + + /// + /// Figure F.15: generate decoding tables for bit-sequential decoding + /// + private void GenerateDecoderTables() + { + short bitcount = 0; + for (int i = 1; i <= 16; i++) + { + if (this.bits[i] != 0) + { + // valoffset[l] = huffval[] index of 1st symbol of code length i, + // minus the minimum code of length i + this.valOffset[i] = (short)(bitcount - this.huffcode[bitcount]); + bitcount += this.bits[i]; + this.maxcode[i] = this.huffcode[bitcount - 1]; // maximum code of length i + } + else + { + this.maxcode[i] = -1; // -1 if no codes of this length + } + } + + this.valOffset[17] = 0; + this.maxcode[17] = 0xFFFFFL; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs index a8644d645..8aeafd7db 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -13,14 +13,14 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// internal class HuffmanTables { - private readonly HuffmanBranch[][] tables = new HuffmanBranch[4][]; + private readonly HuffmanTable[] tables = new HuffmanTable[4]; /// /// Gets or sets the table at the given index. /// /// The index /// The - public HuffmanBranch[] this[int index] + public HuffmanTable this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 5a31fd89f..1d588301f 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -574,6 +574,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components #if DEBUG Debug.WriteLine($"DecodeScan - Unexpected marker {(this.bitsData << 8) | nextByte:X} at {stream.Position}"); #endif + // We've encountered an unexpected marker. Reverse the stream and exit. this.unexpectedMarkerReached = true; stream.Position -= 2; @@ -587,27 +588,31 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private short DecodeHuffman(HuffmanBranch[] tree, Stream stream) + private short DecodeHuffman(HuffmanTable tree, Stream stream) { - // TODO: This is our bottleneck. We should use a faster algorithm with a LUT. - HuffmanBranch[] node = tree; - while (true) + // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81 + int i = 1; + short code = (short)this.ReadBit(stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) { - int index = this.ReadBit(stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) - { - return -1; - } + return -1; + } - HuffmanBranch branch = node[index]; + while (code > tree.GetMaxCode(i)) + { + code <<= 1; + code |= (short)this.ReadBit(stream); - if (branch.Value > -1) + if (this.endOfStreamReached || this.unexpectedMarkerReached) { - return branch.Value; + return -1; } - node = branch.Children; + i++; } + + int j = tree.GetValPtr(i); + return tree.GetHuffVal((j + code) & 0xFF); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -682,6 +687,12 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } k += r; + + if (k > 63) + { + break; + } + byte z = QuantizationTables.DctZigZag[k]; short re = (short)this.ReceiveAndExtend(s, stream); component.BlockData[offset + z] = re; diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 6a1d6311c..7bd71048c 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -7,6 +7,7 @@ namespace ImageSharp.Formats.Jpeg.Port { using System; using System.Collections.Generic; + using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; @@ -233,8 +234,6 @@ namespace ImageSharp.Formats.Jpeg.Port case JpegConstants.Markers.APP15: case JpegConstants.Markers.COM: - - // TODO: Read data block this.InputStream.Skip(remaining); break; @@ -676,28 +675,28 @@ namespace ImageSharp.Formats.Jpeg.Port throw new ImageFormatException($"DHT has wrong length: {remaining}"); } - using (var huffmanData = Buffer.CreateClean(16)) + using (var huffmanData = Buffer.CreateClean(256)) { for (int i = 2; i < remaining;) { byte huffmanTableSpec = (byte)this.InputStream.ReadByte(); this.InputStream.Read(huffmanData.Array, 0, 16); - using (var codeLengths = Buffer.CreateClean(16)) + using (var codeLengths = Buffer.CreateClean(17)) { int codeLengthSum = 0; - for (int j = 0; j < 16; j++) + for (int j = 1; j < 17; j++) { - codeLengthSum += codeLengths[j] = huffmanData[j]; + codeLengthSum += codeLengths[j] = huffmanData[j - 1]; } - using (var huffmanValues = Buffer.CreateClean(codeLengthSum)) + using (var huffmanValues = Buffer.CreateClean(256)) { this.InputStream.Read(huffmanValues.Array, 0, codeLengthSum); i += 17 + codeLengthSum; - + Debug.WriteLine(huffmanTableSpec >> 4 == 0 ? "this.dcHuffmanTables" : "this.acHuffmanTables"); this.BuildHuffmanTable( huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, huffmanTableSpec & 15, @@ -812,53 +811,7 @@ namespace ImageSharp.Formats.Jpeg.Port /// The values private void BuildHuffmanTable(HuffmanTables tables, int index, byte[] codeLengths, byte[] values) { - int length = 16; - while (length > 0 && codeLengths[length - 1] == 0) - { - length--; - } - - // TODO: Check the branch children capacity here. Seems to max at 2 - var code = new List { new HuffmanBranch(-1) }; - HuffmanBranch p = code[0]; - int k = 0; - - for (int i = 0; i < length; i++) - { - HuffmanBranch q; - for (int j = 0; j < codeLengths[i]; j++) - { - p = code.Pop(); - p.Children[p.Index] = new HuffmanBranch(values[k]); - while (p.Index > 0) - { - p = code.Pop(); - } - - p.Index++; - code.Add(p); - while (code.Count <= i) - { - q = new HuffmanBranch(-1); - code.Add(q); - p.Children[p.Index] = new HuffmanBranch(q.Children); - p = q; - } - - k++; - } - - if (i + 1 < length) - { - // p here points to last code - q = new HuffmanBranch(-1); - code.Add(q); - p.Children[p.Index] = new HuffmanBranch(q.Children); - p = q; - } - } - - tables[index] = code[0].Children; + tables[index] = new HuffmanTable(codeLengths, values); } /// From 0f6024247b6e278f6acb183c7abdd6c42a1843b8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Jun 2017 09:54:21 +1000 Subject: [PATCH 26/43] Better Huffman decoding --- .../Formats/Jpeg/Port/Components/Component.cs | 1 + .../Jpeg/Port/Components/FrameComponent.cs | 1 + .../Jpeg/Port/Components/HuffmanBranch.cs | 54 ----------------- .../Jpeg/Port/Components/HuffmanTable.cs | 59 ++++++++++++++----- .../Jpeg/Port/Components/HuffmanTables.cs | 12 +++- .../Jpeg/Port/Components/ScanDecoder.cs | 4 +- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 6 +- 7 files changed, 65 insertions(+), 72 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs index db3170613..120bfb58d 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs @@ -43,6 +43,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components public void Dispose() { this.Output?.Dispose(); + this.Output = null; } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs index 0cb9bbb1c..b386a86f3 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/FrameComponent.cs @@ -68,6 +68,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components public void Dispose() { this.BlockData?.Dispose(); + this.BlockData = null; } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs deleted file mode 100644 index d716355ad..000000000 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanBranch.cs +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright (c) James Jackson-South and contributors. -// Licensed under the Apache License, Version 2.0. -// - -namespace ImageSharp.Formats.Jpeg.Port.Components -{ - using System.Runtime.CompilerServices; - - /// - /// Represents a branch in the huffman tree - /// - internal struct HuffmanBranch - { - /// - /// The index - /// - public int Index; - - /// - /// The value - /// - public short Value; - - /// - /// The children. - /// - public HuffmanBranch[] Children; - - /// - /// Initializes a new instance of the struct. - /// - /// The value - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HuffmanBranch(short value) - { - this.Index = 0; - this.Value = value; - this.Children = new HuffmanBranch[2]; - } - - /// - /// Initializes a new instance of the struct. - /// - /// The branch children - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HuffmanBranch(HuffmanBranch[] children) - { - this.Index = 0; - this.Value = -1; - this.Children = children; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs index 995fd550c..bc3162978 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs @@ -1,32 +1,45 @@ -namespace ImageSharp.Formats.Jpeg.Port.Components +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Jpeg.Port.Components { using System; using System.Runtime.CompilerServices; + using ImageSharp.Memory; + /// - /// Represents a HUffman Table + /// Represents a Huffman Table /// - internal sealed class HuffmanTable + internal struct HuffmanTable : IDisposable { - private short[] huffcode = new short[257]; - private short[] huffsize = new short[257]; - private short[] valOffset = new short[18]; - private long[] maxcode = new long[18]; + private Buffer huffcode; + private Buffer huffsize; + private Buffer valOffset; + private Buffer maxcode; - private byte[] huffval; - private byte[] bits; + private Buffer huffval; + private Buffer bits; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The code lengths /// The huffman values public HuffmanTable(byte[] lengths, byte[] values) { - this.huffval = new byte[values.Length]; - Buffer.BlockCopy(values, 0, this.huffval, 0, values.Length); - this.bits = new byte[lengths.Length]; - Buffer.BlockCopy(lengths, 0, this.bits, 0, lengths.Length); + this.huffcode = Buffer.CreateClean(257); + this.huffsize = Buffer.CreateClean(257); + this.valOffset = Buffer.CreateClean(18); + this.maxcode = Buffer.CreateClean(18); + + this.huffval = Buffer.CreateClean(values.Length); + Buffer.BlockCopy(values, 0, this.huffval.Array, 0, values.Length); + + this.bits = Buffer.CreateClean(lengths.Length); + Buffer.BlockCopy(lengths, 0, this.bits.Array, 0, lengths.Length); this.GenerateSizeTable(); this.GenerateCodeTable(); @@ -66,6 +79,24 @@ return this.valOffset[i]; } + /// + public void Dispose() + { + this.huffcode?.Dispose(); + this.huffsize?.Dispose(); + this.valOffset?.Dispose(); + this.maxcode?.Dispose(); + this.huffval?.Dispose(); + this.bits?.Dispose(); + + this.huffcode = null; + this.huffsize = null; + this.valOffset = null; + this.maxcode = null; + this.huffval = null; + this.bits = null; + } + /// /// Figure C.1: make table of Huffman code length for each symbol /// diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs index 8aeafd7db..6de8c441d 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -5,13 +5,14 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { + using System; using System.Collections.Generic; using System.Runtime.CompilerServices; /// /// Defines a pair of huffman tables /// - internal class HuffmanTables + internal sealed class HuffmanTables : IDisposable { private readonly HuffmanTable[] tables = new HuffmanTable[4]; @@ -34,5 +35,14 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.tables[index] = value; } } + + /// + public void Dispose() + { + for (int i = 0; i < this.tables.Length; i++) + { + this.tables[i].Dispose(); + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 1d588301f..62b0a82e4 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -734,7 +734,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components return; } - var componentBlockDataSpan = component.BlockData.Span; + Span componentBlockDataSpan = component.BlockData.Span; int k = this.specStart; int e = this.specEnd; while (k <= e) @@ -773,7 +773,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int k = this.specStart; int e = this.specEnd; int r = 0; - var componentBlockDataSpan = component.BlockData.Span; + Span componentBlockDataSpan = component.BlockData.Span; while (k <= e) { byte z = QuantizationTables.DctZigZag[k]; diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 7bd71048c..ca93e96dd 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -160,12 +160,16 @@ namespace ImageSharp.Formats.Jpeg.Port this.frame?.Dispose(); this.components?.Dispose(); this.quantizationTables?.Dispose(); + this.dcHuffmanTables?.Dispose(); + this.acHuffmanTables?.Dispose(); this.pixelArea.Dispose(); // Set large fields to null. this.frame = null; this.components = null; this.quantizationTables = null; + this.dcHuffmanTables = null; + this.acHuffmanTables = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -696,7 +700,7 @@ namespace ImageSharp.Formats.Jpeg.Port this.InputStream.Read(huffmanValues.Array, 0, codeLengthSum); i += 17 + codeLengthSum; - Debug.WriteLine(huffmanTableSpec >> 4 == 0 ? "this.dcHuffmanTables" : "this.acHuffmanTables"); + this.BuildHuffmanTable( huffmanTableSpec >> 4 == 0 ? this.dcHuffmanTables : this.acHuffmanTables, huffmanTableSpec & 15, From 0323d000f1f8a297cafc2571f4efa110eab727d3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Jun 2017 16:30:05 +1000 Subject: [PATCH 27/43] Almost got Huffman LUT working --- .../Jpeg/Port/Components/HuffmanTable.cs | 50 ++++++++++++++++++- .../Jpeg/Port/Components/ScanDecoder.cs | 22 ++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs index bc3162978..0119f272c 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs @@ -15,6 +15,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// internal struct HuffmanTable : IDisposable { + private Buffer lookahead; private Buffer huffcode; private Buffer huffsize; private Buffer valOffset; @@ -30,6 +31,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// The huffman values public HuffmanTable(byte[] lengths, byte[] values) { + this.lookahead = Buffer.CreateClean(256); this.huffcode = Buffer.CreateClean(257); this.huffsize = Buffer.CreateClean(257); this.valOffset = Buffer.CreateClean(18); @@ -44,6 +46,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.GenerateSizeTable(); this.GenerateCodeTable(); this.GenerateDecoderTables(); + this.GenerateLookaheadTables(); } /// @@ -74,14 +77,26 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// The index /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetValPtr(int i) + public int GetValOffset(int i) { return this.valOffset[i]; } + /// + /// Gets the look ahead table balue + /// + /// The index + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetLookAhead(int i) + { + return this.lookahead[i]; + } + /// public void Dispose() { + this.lookahead?.Dispose(); this.huffcode?.Dispose(); this.huffsize?.Dispose(); this.valOffset?.Dispose(); @@ -89,6 +104,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.huffval?.Dispose(); this.bits?.Dispose(); + this.lookahead = null; this.huffcode = null; this.huffsize = null; this.valOffset = null; @@ -163,5 +179,37 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.valOffset[17] = 0; this.maxcode[17] = 0xFFFFFL; } + + /// + /// Generates lookup tables to speed up decoding + /// + private void GenerateLookaheadTables() + { + int x = 0, code = 0; + + for (int i = 0; i < 8; i++) + { + code <<= 1; + + for (int j = 0; j < this.bits[i + 1]; 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. + int base2 = code << (7 - i); + int lutValue = (this.huffval[x] << 8) | (2 + i); + + for (int k = 0; k < 1 << (7 - i); k++) + { + this.lookahead[base2 | k] = (short)lutValue; + } + + code++; + x++; + } + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 62b0a82e4..140f47449 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -23,6 +23,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private int bitsCount; + private int accumulator; + private int specStart; private int specEnd; @@ -584,20 +586,34 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } this.bitsCount = 7; + + // TODO: This line is incorrect. + this.accumulator = (this.accumulator << 8) | this.bitsData; + return this.bitsData >> 7; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private short DecodeHuffman(HuffmanTable tree, Stream stream) { - // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81 - int i = 1; short code = (short)this.ReadBit(stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) { return -1; } + // TODO: If the following is enabled the decoder breaks. + // if (this.bitsCount > 0) + // { + // int lutIndex = (this.accumulator >> (this.bitsCount - 7)) & 0xFF; + // int v = tree.GetLookAhead(lutIndex); + // if (v != 0) + // { + // return (short)(v >> 8); + // } + // } + // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81 + int i = 1; while (code > tree.GetMaxCode(i)) { code <<= 1; @@ -611,7 +627,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components i++; } - int j = tree.GetValPtr(i); + int j = tree.GetValOffset(i); return tree.GetHuffVal((j + code) & 0xFF); } From a36061471b9dbd6d06937bbfdd5e91b26545c362 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 Jun 2017 20:02:43 +1000 Subject: [PATCH 28/43] Better Huffman indexing. --- .../Jpeg/Port/Components/HuffmanTables.cs | 10 +-- .../Jpeg/Port/Components/ScanDecoder.cs | 86 +++++++++++-------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs index 6de8c441d..d076c0b03 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTables.cs @@ -21,18 +21,12 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// The index /// The - public HuffmanTable this[int index] + public ref HuffmanTable this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - return this.tables[index]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set - { - this.tables[index] = value; + return ref this.tables[index]; } } diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 140f47449..9648dde82 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -205,6 +205,9 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (componentsLength == 1) { ref FrameComponent component = ref components[this.compIndex]; + ref HuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + for (int n = 0; n < mcuToRead; n++) { if (this.endOfStreamReached || this.unexpectedMarkerReached) @@ -212,7 +215,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; } - this.DecodeBlockBaseline(dcHuffmanTables, acHuffmanTables, ref component, mcu, stream); + this.DecodeBlockBaseline(ref dcHuffmanTable, ref acHuffmanTable, ref component, mcu, stream); mcu++; } } @@ -223,8 +226,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components for (int i = 0; i < componentsLength; i++) { ref FrameComponent component = ref components[i]; + ref HuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; int h = component.HorizontalFactor; int v = component.VerticalFactor; + for (int j = 0; j < v; j++) { for (int k = 0; k < h; k++) @@ -234,7 +240,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; } - this.DecodeMcuBaseline(dcHuffmanTables, acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuBaseline(ref dcHuffmanTable, ref acHuffmanTable, ref component, mcusPerLine, mcu, j, k, stream); } } } @@ -257,6 +263,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (componentsLength == 1) { ref FrameComponent component = ref components[this.compIndex]; + ref HuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; + for (int n = 0; n < mcuToRead; n++) { if (this.endOfStreamReached || this.unexpectedMarkerReached) @@ -264,7 +272,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; } - this.DecodeBlockDCFirst(dcHuffmanTables, ref component, mcu, stream); + this.DecodeBlockDCFirst(ref dcHuffmanTable, ref component, mcu, stream); mcu++; } } @@ -275,8 +283,10 @@ namespace ImageSharp.Formats.Jpeg.Port.Components for (int i = 0; i < componentsLength; i++) { ref FrameComponent component = ref components[i]; + ref HuffmanTable dcHuffmanTable = ref dcHuffmanTables[component.DCHuffmanTableId]; int h = component.HorizontalFactor; int v = component.VerticalFactor; + for (int j = 0; j < v; j++) { for (int k = 0; k < h; k++) @@ -286,7 +296,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; } - this.DecodeMcuDCFirst(dcHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuDCFirst(ref dcHuffmanTable, ref component, mcusPerLine, mcu, j, k, stream); } } } @@ -360,6 +370,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (componentsLength == 1) { ref FrameComponent component = ref components[this.compIndex]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + for (int n = 0; n < mcuToRead; n++) { if (this.endOfStreamReached || this.unexpectedMarkerReached) @@ -367,7 +379,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; } - this.DecodeBlockACFirst(acHuffmanTables, ref component, mcu, stream); + this.DecodeBlockACFirst(ref acHuffmanTable, ref component, mcu, stream); mcu++; } } @@ -378,8 +390,10 @@ namespace ImageSharp.Formats.Jpeg.Port.Components for (int i = 0; i < componentsLength; i++) { ref FrameComponent component = ref components[i]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; int h = component.HorizontalFactor; int v = component.VerticalFactor; + for (int j = 0; j < v; j++) { for (int k = 0; k < h; k++) @@ -389,7 +403,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; } - this.DecodeMcuACFirst(acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuACFirst(ref acHuffmanTable, ref component, mcusPerLine, mcu, j, k, stream); } } } @@ -412,6 +426,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components if (componentsLength == 1) { ref FrameComponent component = ref components[this.compIndex]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; + for (int n = 0; n < mcuToRead; n++) { if (this.endOfStreamReached || this.unexpectedMarkerReached) @@ -419,7 +435,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; } - this.DecodeBlockACSuccessive(acHuffmanTables, ref component, mcu, stream); + this.DecodeBlockACSuccessive(ref acHuffmanTable, ref component, mcu, stream); mcu++; } } @@ -430,8 +446,10 @@ namespace ImageSharp.Formats.Jpeg.Port.Components for (int i = 0; i < componentsLength; i++) { ref FrameComponent component = ref components[i]; + ref HuffmanTable acHuffmanTable = ref acHuffmanTables[component.ACHuffmanTableId]; int h = component.HorizontalFactor; int v = component.VerticalFactor; + for (int j = 0; j < v; j++) { for (int k = 0; k < h; k++) @@ -441,7 +459,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components continue; } - this.DecodeMcuACSuccessive(acHuffmanTables, ref component, mcusPerLine, mcu, j, k, stream); + this.DecodeMcuACSuccessive(ref acHuffmanTable, ref component, mcusPerLine, mcu, j, k, stream); } } } @@ -452,43 +470,43 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockBaseline(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, int mcu, Stream stream) + private void DecodeBlockBaseline(ref HuffmanTable dcHuffmanTable, ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) { int blockRow = (mcu / component.BlocksPerLine) | 0; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); - this.DecodeBaseline(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); + this.DecodeBaseline(ref component, offset, ref dcHuffmanTable, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuBaseline(HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuBaseline(ref HuffmanTable dcHuffmanTable, ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { int mcuRow = (mcu / mcusPerLine) | 0; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; int offset = GetBlockBufferOffset(component, blockRow, blockCol); - this.DecodeBaseline(ref component, offset, dcHuffmanTables, acHuffmanTables, stream); + this.DecodeBaseline(ref component, offset, ref dcHuffmanTable, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockDCFirst(HuffmanTables dcHuffmanTables, ref FrameComponent component, int mcu, Stream stream) + private void DecodeBlockDCFirst(ref HuffmanTable dcHuffmanTable, ref FrameComponent component, int mcu, Stream stream) { int blockRow = (mcu / component.BlocksPerLine) | 0; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); - this.DecodeDCFirst(ref component, offset, dcHuffmanTables, stream); + this.DecodeDCFirst(ref component, offset, ref dcHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuDCFirst(HuffmanTables dcHuffmanTables, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuDCFirst(ref HuffmanTable dcHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { int mcuRow = (mcu / mcusPerLine) | 0; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; int offset = GetBlockBufferOffset(component, blockRow, blockCol); - this.DecodeDCFirst(ref component, offset, dcHuffmanTables, stream); + this.DecodeDCFirst(ref component, offset, ref dcHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -512,43 +530,43 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockACFirst(HuffmanTables acHuffmanTables, ref FrameComponent component, int mcu, Stream stream) + private void DecodeBlockACFirst(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) { int blockRow = (mcu / component.BlocksPerLine) | 0; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); - this.DecodeACFirst(ref component, offset, acHuffmanTables, stream); + this.DecodeACFirst(ref component, offset, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuACFirst(HuffmanTables acHuffmanTables, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuACFirst(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { int mcuRow = (mcu / mcusPerLine) | 0; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; int offset = GetBlockBufferOffset(component, blockRow, blockCol); - this.DecodeACFirst(ref component, offset, acHuffmanTables, stream); + this.DecodeACFirst(ref component, offset, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBlockACSuccessive(HuffmanTables acHuffmanTables, ref FrameComponent component, int mcu, Stream stream) + private void DecodeBlockACSuccessive(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) { int blockRow = (mcu / component.BlocksPerLine) | 0; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); - this.DecodeACSuccessive(ref component, offset, acHuffmanTables, stream); + this.DecodeACSuccessive(ref component, offset, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeMcuACSuccessive(HuffmanTables acHuffmanTables, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) + private void DecodeMcuACSuccessive(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { int mcuRow = (mcu / mcusPerLine) | 0; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; int offset = GetBlockBufferOffset(component, blockRow, blockCol); - this.DecodeACSuccessive(ref component, offset, acHuffmanTables, stream); + this.DecodeACSuccessive(ref component, offset, ref acHuffmanTable, stream); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -594,7 +612,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private short DecodeHuffman(HuffmanTable tree, Stream stream) + private short DecodeHuffman(ref HuffmanTable tree, Stream stream) { short code = (short)this.ReadBit(stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) @@ -668,9 +686,9 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeBaseline(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, HuffmanTables acHuffmanTables, Stream stream) + private void DecodeBaseline(ref FrameComponent component, int offset, ref HuffmanTable dcHuffmanTable, ref HuffmanTable acHuffmanTable, Stream stream) { - int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); + int t = this.DecodeHuffman(ref dcHuffmanTable, stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; @@ -682,7 +700,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int k = 1; while (k < 64) { - int rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + int rs = this.DecodeHuffman(ref acHuffmanTable, stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; @@ -717,9 +735,9 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeDCFirst(ref FrameComponent component, int offset, HuffmanTables dcHuffmanTables, Stream stream) + private void DecodeDCFirst(ref FrameComponent component, int offset, ref HuffmanTable dcHuffmanTable, Stream stream) { - int t = this.DecodeHuffman(dcHuffmanTables[component.DCHuffmanTableId], stream); + int t = this.DecodeHuffman(ref dcHuffmanTable, stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; @@ -742,7 +760,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeACFirst(ref FrameComponent component, int offset, HuffmanTables acHuffmanTables, Stream stream) + private void DecodeACFirst(ref FrameComponent component, int offset, ref HuffmanTable acHuffmanTable, Stream stream) { if (this.eobrun > 0) { @@ -755,7 +773,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int e = this.specEnd; while (k <= e) { - short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + short rs = this.DecodeHuffman(ref acHuffmanTable, stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; @@ -784,7 +802,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DecodeACSuccessive(ref FrameComponent component, int offset, HuffmanTables acHuffmanTables, Stream stream) + private void DecodeACSuccessive(ref FrameComponent component, int offset, ref HuffmanTable acHuffmanTable, Stream stream) { int k = this.specStart; int e = this.specEnd; @@ -796,7 +814,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components switch (this.successiveACState) { case 0: // Initial state - short rs = this.DecodeHuffman(acHuffmanTables[component.ACHuffmanTableId], stream); + short rs = this.DecodeHuffman(ref acHuffmanTable, stream); if (this.endOfStreamReached || this.unexpectedMarkerReached) { return; From 824131429492c768d157590c4871752c1ff86f64 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 1 Jul 2017 13:34:50 +1000 Subject: [PATCH 29/43] Getting close with that lookup now. --- .../Jpeg/Port/Components/HuffmanTable.cs | 6 +- .../Jpeg/Port/Components/ScanDecoder.cs | 57 +++++++++++++------ 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs index 0119f272c..85bcb5dfe 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs @@ -198,12 +198,12 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // 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. - int base2 = code << (7 - i); - int lutValue = (this.huffval[x] << 8) | (2 + i); + byte base2 = (byte)(code << (7 - i)); + short lutValue = (short)((short)(this.huffval[x] << 8) | (short)(2 + i)); for (int k = 0; k < 1 << (7 - i); k++) { - this.lookahead[base2 | k] = (short)lutValue; + this.lookahead[base2 | k] = lutValue; } code++; diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 9648dde82..19ffcd778 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -23,6 +23,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private int bitsCount; + private int bitsUnRead; + private int accumulator; private int specStart; @@ -139,6 +141,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // Find marker this.bitsCount = 0; + this.accumulator = 0; + this.bitsUnRead = 0; fileMarker = JpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream); // Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past @@ -572,6 +576,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private int ReadBit(Stream stream) { + // TODO: I wonder if we can do this two bytes at a time; libjpeg turbo seems to do that? if (this.bitsCount > 0) { this.bitsCount--; @@ -586,7 +591,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.endOfStreamReached = true; } - if (this.bitsData == 0xFF) + if (this.bitsData == JpegConstants.Markers.Prefix) { int nextByte = stream.ReadByte(); if (nextByte != 0) @@ -605,33 +610,51 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.bitsCount = 7; - // TODO: This line is incorrect. - this.accumulator = (this.accumulator << 8) | this.bitsData; - return this.bitsData >> 7; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private short DecodeHuffman(ref HuffmanTable tree, Stream stream) { - short code = (short)this.ReadBit(stream); - if (this.endOfStreamReached || this.unexpectedMarkerReached) - { - return -1; - } + short code = -1; - // TODO: If the following is enabled the decoder breaks. - // if (this.bitsCount > 0) + // TODO: Adding this code introduces error into the decoder. + // It doesn't appear to speed anything up either. + // if (this.bitsUnRead < 8) // { - // int lutIndex = (this.accumulator >> (this.bitsCount - 7)) & 0xFF; - // int v = tree.GetLookAhead(lutIndex); - // if (v != 0) - // { - // return (short)(v >> 8); - // } + // if (this.bitsCount <= 0) + // { + // code = (short)this.ReadBit(stream); + // this.bitsUnRead += 8; + // } + // if (this.endOfStreamReached || this.unexpectedMarkerReached) + // { + // return -1; + // } + // this.accumulator = (this.accumulator << 8) | this.bitsData; + // int lutIndex = (this.accumulator >> (this.bitsUnRead - 8)) & 0xFF; + // int v = tree.GetLookAhead(lutIndex); + // if (v != 0) + // { + // int nb = (v & 0xFF) - 1; + // this.bitsCount -= nb - 1; + // this.bitsUnRead -= nb; + // v = v >> 8; + // return (short)v; + // } // } + if (code == -1) + { + code = (short)this.ReadBit(stream); + if (this.endOfStreamReached || this.unexpectedMarkerReached) + { + return -1; + } + } + // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81 int i = 1; + while (code > tree.GetMaxCode(i)) { code <<= 1; From 2c64ed6b2dcc76bf72828cf898ad61476dffc0ef Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 1 Jul 2017 13:38:32 +1000 Subject: [PATCH 30/43] Fix build --- src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 19ffcd778..98d46dae5 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -23,9 +23,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components private int bitsCount; +#pragma warning disable 414 private int bitsUnRead; private int accumulator; +#pragma warning restore 414 private int specStart; From 5086b08b958941020d221c974e4a8cbc1807c34a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 2 Jul 2017 17:15:02 +1000 Subject: [PATCH 31/43] Use properties --- .../Jpeg/Port/Components/HuffmanTable.cs | 125 +++++++++--------- .../Jpeg/Port/Components/ScanDecoder.cs | 8 +- 2 files changed, 65 insertions(+), 68 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs index 85bcb5dfe..4c475450b 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/HuffmanTable.cs @@ -16,13 +16,9 @@ namespace ImageSharp.Formats.Jpeg.Port.Components internal struct HuffmanTable : IDisposable { private Buffer lookahead; - private Buffer huffcode; - private Buffer huffsize; private Buffer valOffset; private Buffer maxcode; - private Buffer huffval; - private Buffer bits; /// /// Initializes a new instance of the struct. @@ -32,119 +28,113 @@ namespace ImageSharp.Formats.Jpeg.Port.Components public HuffmanTable(byte[] lengths, byte[] values) { this.lookahead = Buffer.CreateClean(256); - this.huffcode = Buffer.CreateClean(257); - this.huffsize = Buffer.CreateClean(257); this.valOffset = Buffer.CreateClean(18); this.maxcode = Buffer.CreateClean(18); + using (var huffsize = Buffer.CreateClean(257)) + using (var huffcode = Buffer.CreateClean(257)) + { + GenerateSizeTable(lengths, huffsize); + GenerateCodeTable(huffsize, huffcode); + GenerateDecoderTables(lengths, huffcode, this.valOffset, this.maxcode); + GenerateLookaheadTables(lengths, values, this.lookahead); + } + this.huffval = Buffer.CreateClean(values.Length); Buffer.BlockCopy(values, 0, this.huffval.Array, 0, values.Length); - this.bits = Buffer.CreateClean(lengths.Length); - Buffer.BlockCopy(lengths, 0, this.bits.Array, 0, lengths.Length); - - this.GenerateSizeTable(); - this.GenerateCodeTable(); - this.GenerateDecoderTables(); - this.GenerateLookaheadTables(); + this.MaxCode = this.maxcode.Array; + this.ValOffset = this.valOffset.Array; + this.HuffVal = this.huffval.Array; + this.Lookahead = this.lookahead.Array; } /// - /// Gets the Huffman value code at the given index + /// Gets the max code array /// - /// The index - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public short GetHuffVal(int i) + public long[] MaxCode { - return this.huffval[i]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; } /// - /// Gets the max code at the given index + /// Gets the value offset array /// - /// The index - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetMaxCode(int i) + public short[] ValOffset { - return this.maxcode[i]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; } /// - /// Gets the index to the locatation of the huffman value + /// Gets the huffman value array /// - /// The index - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetValOffset(int i) + public byte[] HuffVal { - return this.valOffset[i]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; } /// - /// Gets the look ahead table balue + /// Gets the lookahead array /// - /// The index - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetLookAhead(int i) + public short[] Lookahead { - return this.lookahead[i]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; } /// public void Dispose() { this.lookahead?.Dispose(); - this.huffcode?.Dispose(); - this.huffsize?.Dispose(); this.valOffset?.Dispose(); this.maxcode?.Dispose(); this.huffval?.Dispose(); - this.bits?.Dispose(); this.lookahead = null; - this.huffcode = null; - this.huffsize = null; this.valOffset = null; this.maxcode = null; this.huffval = null; - this.bits = null; } /// /// Figure C.1: make table of Huffman code length for each symbol /// - private void GenerateSizeTable() + /// The code lengths + /// The huffman size span + private static void GenerateSizeTable(byte[] lengths, Span huffsize) { short index = 0; for (short l = 1; l <= 16; l++) { - byte i = this.bits[l]; + byte i = lengths[l]; for (short j = 0; j < i; j++) { - this.huffsize[index] = l; + huffsize[index] = l; index++; } } - this.huffsize[index] = 0; + huffsize[index] = 0; } /// /// Figure C.2: generate the codes themselves /// - private void GenerateCodeTable() + /// The huffman size span + /// The huffman code span + private static void GenerateCodeTable(Span huffsize, Span huffcode) { short k = 0; - short si = this.huffsize[0]; + short si = huffsize[0]; short code = 0; - for (short i = 0; i < this.huffsize.Length; i++) + for (short i = 0; i < huffsize.Length; i++) { - while (this.huffsize[k] == si) + while (huffsize[k] == si) { - this.huffcode[k] = code; + huffcode[k] = code; code++; k++; } @@ -157,33 +147,40 @@ namespace ImageSharp.Formats.Jpeg.Port.Components /// /// Figure F.15: generate decoding tables for bit-sequential decoding /// - private void GenerateDecoderTables() + /// The code lengths + /// The huffman code span + /// The value offset span + /// The max code span + private static void GenerateDecoderTables(byte[] lengths, Span huffcode, Span valOffset, Span maxcode) { short bitcount = 0; for (int i = 1; i <= 16; i++) { - if (this.bits[i] != 0) + if (lengths[i] != 0) { // valoffset[l] = huffval[] index of 1st symbol of code length i, // minus the minimum code of length i - this.valOffset[i] = (short)(bitcount - this.huffcode[bitcount]); - bitcount += this.bits[i]; - this.maxcode[i] = this.huffcode[bitcount - 1]; // maximum code of length i + valOffset[i] = (short)(bitcount - huffcode[bitcount]); + bitcount += lengths[i]; + maxcode[i] = huffcode[bitcount - 1]; // maximum code of length i } else { - this.maxcode[i] = -1; // -1 if no codes of this length + maxcode[i] = -1; // -1 if no codes of this length } } - this.valOffset[17] = 0; - this.maxcode[17] = 0xFFFFFL; + valOffset[17] = 0; + maxcode[17] = 0xFFFFFL; } /// /// Generates lookup tables to speed up decoding /// - private void GenerateLookaheadTables() + /// The code lengths + /// The huffman value array + /// The lookahead span + private static void GenerateLookaheadTables(byte[] lengths, byte[] huffval, Span lookahead) { int x = 0, code = 0; @@ -191,7 +188,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { code <<= 1; - for (int j = 0; j < this.bits[i + 1]; j++) + for (int j = 0; j < lengths[i + 1]; j++) { // The codeLength is 1+i, so shift code by 8-(1+i) to // calculate the high bits for every 8-bit sequence @@ -199,11 +196,11 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // 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)); - short lutValue = (short)((short)(this.huffval[x] << 8) | (short)(2 + i)); + short lutValue = (short)((short)(huffval[x] << 8) | (short)(2 + i)); for (int k = 0; k < 1 << (7 - i); k++) { - this.lookahead[base2 | k] = lutValue; + lookahead[base2 | k] = lutValue; } code++; diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 98d46dae5..23288f036 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -635,7 +635,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // } // this.accumulator = (this.accumulator << 8) | this.bitsData; // int lutIndex = (this.accumulator >> (this.bitsUnRead - 8)) & 0xFF; - // int v = tree.GetLookAhead(lutIndex); + // int v = tree.Lookahead[lutIndex]; // if (v != 0) // { // int nb = (v & 0xFF) - 1; @@ -657,7 +657,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // "DECODE", section F.2.2.3, figure F.16, page 109 of T.81 int i = 1; - while (code > tree.GetMaxCode(i)) + while (code > tree.MaxCode[i]) { code <<= 1; code |= (short)this.ReadBit(stream); @@ -670,8 +670,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components i++; } - int j = tree.GetValOffset(i); - return tree.GetHuffVal((j + code) & 0xFF); + int j = tree.ValOffset[i]; + return tree.HuffVal[(j + code) & 0xFF]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 290646afcb711c6da95369cd8d041f160c849247 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sun, 2 Jul 2017 20:05:27 +0100 Subject: [PATCH 32/43] store scale as vector + remove unneeded bit or operations `var t = blah | 0;` in js is for trimming floats into ints c# doesn't need it --- .../Formats/Jpeg/Port/Components/Component.cs | 11 +++------- .../Jpeg/Port/Components/JpegPixelArea.cs | 11 +++++----- .../Jpeg/Port/Components/ScanDecoder.cs | 20 +++++++++---------- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 5 +++-- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs index 120bfb58d..a21cb6620 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/Component.cs @@ -6,7 +6,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { using System; - + using System.Numerics; using ImageSharp.Memory; /// @@ -20,14 +20,9 @@ namespace ImageSharp.Formats.Jpeg.Port.Components public Buffer Output; /// - /// Gets or sets the horizontal scaling factor - /// - public float ScaleX; - - /// - /// Gets or sets the vertical scaling factor + /// Gets or sets the scaling factors /// - public float ScaleY; + public Vector2 Scale; /// /// Gets or sets the number of blocks per line diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs index 33cc44df4..e88e39617 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/JpegPixelArea.cs @@ -7,6 +7,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { using System; using System.Diagnostics; + using System.Numerics; using System.Runtime.CompilerServices; using ImageSharp.Memory; @@ -68,9 +69,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components this.Height = height; int numberOfComponents = this.NumberOfComponents; this.rowStride = width * numberOfComponents; + var scale = new Vector2(this.imageWidth / (float)width, this.imageHeight / (float)height); - float scaleX = this.imageWidth / (float)width; - float scaleY = this.imageHeight / (float)height; this.componentData = new Buffer(width * height * numberOfComponents); Span componentDataSpan = this.componentData; const uint Mask3Lsb = 0xFFFFFFF8; // Used to clear the 3 LSBs @@ -81,8 +81,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components for (int i = 0; i < numberOfComponents; i++) { ref Component component = ref components.Components[i]; - float componentScaleX = component.ScaleX * scaleX; - float componentScaleY = component.ScaleY * scaleY; + Vector2 componentScale = component.Scale * scale; int offset = i; Span output = component.Output; int blocksPerScanline = (component.BlocksPerLine + 1) << 3; @@ -91,14 +90,14 @@ namespace ImageSharp.Formats.Jpeg.Port.Components int j; for (int x = 0; x < width; x++) { - j = 0 | (int)(x * componentScaleX); + j = (int)(x * componentScale.X); xScaleBlockOffsetSpan[x] = (int)((j & Mask3Lsb) << 3) | (j & 7); } // Linearize the blocks of the component for (int y = 0; y < height; y++) { - j = 0 | (int)(y * componentScaleY); + j = (int)(y * componentScale.Y); int index = blocksPerScanline * (int)(j & Mask3Lsb) | ((j & 7) << 3); for (int x = 0; x < width; x++) { diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 23288f036..47ec50d71 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -478,7 +478,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeBlockBaseline(ref HuffmanTable dcHuffmanTable, ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) { - int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockRow = mcu / component.BlocksPerLine; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); this.DecodeBaseline(ref component, offset, ref dcHuffmanTable, ref acHuffmanTable, stream); @@ -487,7 +487,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeMcuBaseline(ref HuffmanTable dcHuffmanTable, ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { - int mcuRow = (mcu / mcusPerLine) | 0; + int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; @@ -498,7 +498,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeBlockDCFirst(ref HuffmanTable dcHuffmanTable, ref FrameComponent component, int mcu, Stream stream) { - int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockRow = mcu / component.BlocksPerLine; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); this.DecodeDCFirst(ref component, offset, ref dcHuffmanTable, stream); @@ -507,7 +507,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeMcuDCFirst(ref HuffmanTable dcHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { - int mcuRow = (mcu / mcusPerLine) | 0; + int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; @@ -518,7 +518,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeBlockDCSuccessive(ref FrameComponent component, int mcu, Stream stream) { - int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockRow = mcu / component.BlocksPerLine; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); this.DecodeDCSuccessive(ref component, offset, stream); @@ -527,7 +527,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeMcuDCSuccessive(ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { - int mcuRow = (mcu / mcusPerLine) | 0; + int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; @@ -538,7 +538,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeBlockACFirst(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) { - int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockRow = mcu / component.BlocksPerLine; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); this.DecodeACFirst(ref component, offset, ref acHuffmanTable, stream); @@ -547,7 +547,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeMcuACFirst(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { - int mcuRow = (mcu / mcusPerLine) | 0; + int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; @@ -558,7 +558,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeBlockACSuccessive(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcu, Stream stream) { - int blockRow = (mcu / component.BlocksPerLine) | 0; + int blockRow = mcu / component.BlocksPerLine; int blockCol = mcu % component.BlocksPerLine; int offset = GetBlockBufferOffset(component, blockRow, blockCol); this.DecodeACSuccessive(ref component, offset, ref acHuffmanTable, stream); @@ -567,7 +567,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecodeMcuACSuccessive(ref HuffmanTable acHuffmanTable, ref FrameComponent component, int mcusPerLine, int mcu, int row, int col, Stream stream) { - int mcuRow = (mcu / mcusPerLine) | 0; + int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; int blockRow = (mcuRow * component.VerticalFactor) + row; int blockCol = (mcuCol * component.HorizontalFactor) + col; diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index f0e05fabd..d7f8a3ae7 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -277,8 +277,9 @@ namespace ImageSharp.Formats.Jpeg.Port ref var frameComponent = ref this.frame.Components[i]; var component = new Component { - ScaleX = frameComponent.HorizontalFactor / (float)this.frame.MaxHorizontalFactor, - ScaleY = frameComponent.VerticalFactor / (float)this.frame.MaxVerticalFactor, + Scale = new System.Numerics.Vector2( + frameComponent.HorizontalFactor / (float)this.frame.MaxHorizontalFactor, + frameComponent.VerticalFactor / (float)this.frame.MaxVerticalFactor), BlocksPerLine = frameComponent.BlocksPerLine, BlocksPerColumn = frameComponent.BlocksPerColumn }; From 738fc202a708df5ac7ad104b84166c1c8f504822 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 6 Jul 2017 23:29:36 +1000 Subject: [PATCH 33/43] Add WIP faster IDCT method This is killing me! --- .../Formats/Jpeg/Port/Components/IDCT.cs | 248 +++++++++++++++++- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 2 +- 2 files changed, 248 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs index f931c3d6b..6b77dddb1 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -1,6 +1,9 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { using System; + using System.Numerics; + using System.Runtime.CompilerServices; + using ImageSharp.Memory; /// @@ -17,6 +20,47 @@ private const int DctSqrt2 = 5793; // sqrt(2) private const int DctSqrt1D2 = 2896; // sqrt(2) / 2 +#pragma warning disable SA1310 // Field names must not contain underscore + private const int FIX_1_082392200 = 277; /* FIX(1.082392200) */ + private const int FIX_1_414213562 = 362; /* FIX(1.414213562) */ + private const int FIX_1_847759065 = 473; /* FIX(1.847759065) */ + private const int FIX_2_613125930 = 669; /* FIX(2.613125930) */ +#pragma warning restore SA1310 // Field names must not contain underscore + + private const int ScaleBits = 2; /* fractional bits in scale factors */ + + /* + * Each IDCT routine is responsible for range-limiting its results and + * converting them to unsigned form (0..255). The raw outputs could + * be quite far out of range if the input data is corrupt, so a bulletproof + * range-limiting step is required. We use a mask-and-table-lookup method + * to do the combined operations quickly, assuming that 255+1 + * is a power of 2. See the comments with prepare_range_limit_table for more info. + */ + private const int RangeMask = (255 * 4) + 3; /* 2 bits wider than legal samples */ + + private static readonly byte[] Limit = new byte[5 * (255 + 1)]; + + static IDCT() + { + // First segment of range limit table: limit[x] = 0 for x < 0 + // allow negative subscripts of simple table */ + int tableOffset = 2 * (255 + 1); + + // Main part of range limit table: limit[x] = x + int i; + for (i = 0; i <= 255; i++) + { + Limit[tableOffset + i] = (byte)i; + } + + /* End of range limit table: limit[x] = MAXJSAMPLE for x > MAXJSAMPLE */ + for (; i < 3 * (255 + 1); i++) + { + Limit[tableOffset + i] = 255; + } + } + /// /// A port of Poppler's IDCT method which in turn is taken from: /// Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz, @@ -219,5 +263,207 @@ blockData[col + 56] = (short)p7; } } + + /// + /// A port of + /// TODO: This does not work!! + /// A 2-D IDCT can be done by 1-D IDCT on each column followed by 1-D IDCT + /// on each row(or vice versa, but it's more convenient to emit a row at + /// a time). Direct algorithms are also available, but they are much more + /// complex and seem not to be any faster when reduced to code. + /// + /// This implementation is based on Arai, Agui, and Nakajima's algorithm for + /// scaled DCT.Their original paper (Trans.IEICE E-71(11):1095) is in + /// Japanese, but the algorithm is described in the Pennebaker & Mitchell + /// JPEG textbook(see REFERENCES section in file README.ijg). The following + /// code is based directly on figure 4-8 in P&M. + /// While an 8-point DCT cannot be done in less than 11 multiplies, it is + /// possible to arrange the computation so that many of the multiplies are + /// simple scalings of the final outputs.These multiplies can then be + /// folded into the multiplications or divisions by the JPEG quantization + /// table entries. The AA&N method leaves only 5 multiplies and 29 adds + /// to be done in the DCT itself. + /// The primary disadvantage of this method is that with fixed-point math, + /// accuracy is lost due to imprecise representation of the scaled + /// quantization values.The smaller the quantization table entry, the less + /// precise the scaled value, so this implementation does worse with high - + /// quality - setting files than with low - quality ones. + /// + /// The quantization tables + /// The fram component + /// The block buffer offset + /// The computational buffer for holding temp values + public static void QuantizeAndInverseAlt(QuantizationTables quantizationTables, ref FrameComponent component, int blockBufferOffset, Buffer computationBuffer) + { + Span qt = quantizationTables.Tables.GetRowSpan(component.QuantizationIdentifier); + Span blockData = component.BlockData.Slice(blockBufferOffset); + Span computationBufferSpan = computationBuffer; + + int p0, p1, p2, p3, p4, p5, p6, p7; + + for (int col = 0; col < 8; col++) + { + // Gather block data + p0 = blockData[col]; + p1 = blockData[col + 8]; + p2 = blockData[col + 16]; + p3 = blockData[col + 24]; + p4 = blockData[col + 32]; + p5 = blockData[col + 40]; + p6 = blockData[col + 48]; + p7 = blockData[col + 56]; + + int tmp0 = p0 * qt[col]; + + // Check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + short dcval = (short)tmp0; + + computationBufferSpan[col] = dcval; + computationBufferSpan[col + 8] = dcval; + computationBufferSpan[col + 16] = dcval; + computationBufferSpan[col + 24] = dcval; + computationBufferSpan[col + 32] = dcval; + computationBufferSpan[col + 40] = dcval; + computationBufferSpan[col + 48] = dcval; + computationBufferSpan[col + 56] = dcval; + continue; + } + + // Even part + int tmp1 = p2 * qt[col + 16]; + int tmp2 = p4 * qt[col + 32]; + int tmp3 = p6 * qt[col + 48]; + + int tmp10 = tmp0 + tmp2; // Phase 3 + int tmp11 = tmp0 - tmp2; + + int tmp13 = tmp1 + tmp3; // Phases 5-3 + int tmp12 = Multiply(tmp1 - tmp3, FIX_1_414213562) - tmp13; // 2*c4 + + tmp0 = tmp10 + tmp13; // Phase 2 + tmp3 = tmp10 - tmp13; + tmp1 = tmp11 + tmp12; + tmp2 = tmp11 - tmp12; + + // Odd Part + int tmp4 = p1 * qt[col + 8]; + int tmp5 = p3 * qt[col + 24]; + int tmp6 = p5 * qt[col + 40]; + int tmp7 = p7 * qt[col + 56]; + + int z13 = tmp6 + tmp5; // Phase 6 + int z10 = tmp6 - tmp5; + int z11 = tmp4 + tmp7; + int z12 = tmp4 - tmp7; + + tmp7 = z11 + z13; // Phase 5 + tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 + + int z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 + tmp10 = Multiply(z12, FIX_1_082392200) - z5; // 2*(c2-c6) + tmp12 = Multiply(z10, FIX_2_613125930) + z5; // 2*(c2+c6) + + tmp6 = tmp12 - tmp7; // Phase 2 + tmp5 = tmp11 - tmp6; + tmp4 = tmp10 - tmp5; + + computationBufferSpan[col] = (short)(tmp0 + tmp7); + computationBufferSpan[col + 56] = (short)(tmp0 - tmp7); + computationBufferSpan[col + 8] = (short)(tmp1 + tmp6); + computationBufferSpan[col + 48] = (short)(tmp1 - tmp6); + computationBufferSpan[col + 16] = (short)(tmp2 + tmp5); + computationBufferSpan[col + 40] = (short)(tmp2 - tmp5); + computationBufferSpan[col + 32] = (short)(tmp3 + tmp4); + computationBufferSpan[col + 24] = (short)(tmp3 - tmp4); + } + + // Pass 2: process rows from work array, store into output array. + // Note that we must descale the results by a factor of 8 == 2**3, + // and also undo the pass 1 bits scaling. + for (int row = 0; row < 64; row += 8) + { + p0 = computationBufferSpan[row]; + p1 = computationBufferSpan[row + 1]; + p2 = computationBufferSpan[row + 2]; + p3 = computationBufferSpan[row + 3]; + p4 = computationBufferSpan[row + 4]; + p5 = computationBufferSpan[row + 5]; + p6 = computationBufferSpan[row + 6]; + p7 = computationBufferSpan[row + 7]; + + // Check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + byte dcval = Limit[Descale(p0, ScaleBits + 3) & RangeMask]; + + blockData[row] = dcval; + blockData[row + 1] = dcval; + blockData[row + 2] = dcval; + blockData[row + 3] = dcval; + blockData[row + 4] = dcval; + blockData[row + 5] = dcval; + blockData[row + 6] = dcval; + blockData[row + 7] = dcval; + + continue; + } + + // Even part + int tmp10 = p0 + p4; + int tmp11 = p0 - p4; + + int tmp13 = p2 + p6; + int tmp12 = Multiply(p2 - p6, FIX_1_414213562) - tmp13; /* 2*c4 */ + + int tmp0 = tmp10 + tmp13; + int tmp3 = tmp10 - tmp13; + int tmp1 = tmp11 + tmp12; + int tmp2 = tmp11 - tmp12; + + // Odd part + int z13 = p5 + p3; + int z10 = p5 - p3; + int z11 = p1 + p7; + int z12 = p1 - p7; + + int tmp7 = z11 + z13; // Phase 5 + tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 + + int z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 + tmp10 = Multiply(z12, FIX_1_082392200) - z5; // 2*(c2-c6) + tmp12 = Multiply(z10, FIX_2_613125930) + z5; // -2*(c2+c6) + + int tmp6 = tmp12 - tmp7; // Phase 2 + int tmp5 = tmp11 - tmp6; + int tmp4 = tmp10 - tmp5; + + // Final output stage: scale down by a factor of 8 and range-limit + blockData[row] = Limit[Descale(tmp0 + tmp7, ScaleBits + 3) & RangeMask]; + blockData[row + 7] = Limit[Descale(tmp0 - tmp7, ScaleBits + 3) & RangeMask]; + blockData[row + 1] = Limit[Descale(tmp1 + tmp6, ScaleBits + 3) & RangeMask]; + blockData[row + 6] = Limit[Descale(tmp1 - tmp6, ScaleBits + 3) & RangeMask]; + blockData[row + 2] = Limit[Descale(tmp2 + tmp5, ScaleBits + 3) & RangeMask]; + blockData[row + 5] = Limit[Descale(tmp2 - tmp5, ScaleBits + 3) & RangeMask]; + blockData[row + 3] = Limit[Descale(tmp3 + tmp4, ScaleBits + 3) & RangeMask]; + blockData[row + 4] = Limit[Descale(tmp3 - tmp4, ScaleBits + 3) & RangeMask]; + } + } + + private static int Multiply(int val, int c) + { + return Descale(val * c, 8); + } + + private static int RightShift(int x, int shft) + { + return x >> shft; + } + + private static int Descale(int x, int n) + { + return RightShift(x + (1 << (n - 1)), n); + } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index d7f8a3ae7..ef49dfaf0 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -798,7 +798,7 @@ namespace ImageSharp.Formats.Jpeg.Port for (int blockCol = 0; blockCol < blocksPerLine; blockCol++) { int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); - IDCT.QuantizeAndInverse(this.quantizationTables, ref frameComponent, offset, computationBuffer); + IDCT.QuantizeAndInverseAlt(this.quantizationTables, ref frameComponent, offset, computationBuffer); } } } From 9ff1b566086f56321b87b3fc53c31f6b913d3128 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 9 Jul 2017 10:54:27 +1000 Subject: [PATCH 34/43] This works. High allocations though --- ImageSharp.sln | 5 +- .../Formats/Jpeg/Port/Components/IDCT.cs | 415 ++++++++++-------- 2 files changed, 244 insertions(+), 176 deletions(-) diff --git a/ImageSharp.sln b/ImageSharp.sln index a584c5686..aedfcc345 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -49,9 +49,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{7CC6 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvatarWithRoundedCorner", "samples\AvatarWithRoundedCorner\AvatarWithRoundedCorner.csproj", "{844FC582-4E78-4371-847D-EFD4D1103578}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChangeDefaultEncoderOptions", "samples\ChangeDefaultEncoderOptions\ChangeDefaultEncoderOptions.csproj", "{07EE511D-4BAB-4323-BAFC-3AF2BF9366F0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeDefaultEncoderOptions", "samples\ChangeDefaultEncoderOptions\ChangeDefaultEncoderOptions.csproj", "{07EE511D-4BAB-4323-BAFC-3AF2BF9366F0}" EndProject Global + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs index 6b77dddb1..22d787269 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -27,37 +27,52 @@ private const int FIX_2_613125930 = 669; /* FIX(2.613125930) */ #pragma warning restore SA1310 // Field names must not contain underscore - private const int ScaleBits = 2; /* fractional bits in scale factors */ - - /* - * Each IDCT routine is responsible for range-limiting its results and - * converting them to unsigned form (0..255). The raw outputs could - * be quite far out of range if the input data is corrupt, so a bulletproof - * range-limiting step is required. We use a mask-and-table-lookup method - * to do the combined operations quickly, assuming that 255+1 - * is a power of 2. See the comments with prepare_range_limit_table for more info. - */ - private const int RangeMask = (255 * 4) + 3; /* 2 bits wider than legal samples */ + private const int ConstBits = 8; + private const int Pass1Bits = 2; // Factional bits in scale factors + private const int MaxJSample = 255; + private const int CenterJSample = 128; + private const int RangeCenter = (MaxJSample * 2) + 2; + + // First segment of range limit table: limit[x] = 0 for x < 0 + // allow negative subscripts of simple table + private const int TableOffset = 2 * (MaxJSample + 1); + private const int LimitOffset = TableOffset - (RangeCenter - CenterJSample); + + // Each IDCT routine is responsible for range-limiting its results and + // converting them to unsigned form (0..MaxJSample). The raw outputs could + // be quite far out of range if the input data is corrupt, so a bulletproof + // range-limiting step is required. We use a mask-and-table-lookup method + // to do the combined operations quickly, assuming that MaxJSample+1 + // is a power of 2. See the comments with prepare_range_limit_table for more info. + private const int RangeMask = (MaxJSample * 4) + 3; // 2 bits wider than legal samples + + // Precomputed values scaled up by 14 bits + private static readonly short[] Aanscales = + { + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, 22725, 31521, 29692, 26722, 22725, 17855, + 12299, 6270, 21407, 29692, 27969, 25172, 21407, 16819, 11585, + 5906, 19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315, + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, 12873, + 17855, 16819, 15137, 12873, 10114, 6967, 3552, 8867, 12299, + 11585, 10426, 8867, 6967, 4799, 2446, 4520, 6270, 5906, 5315, + 4520, 3552, 2446, 1247 + }; - private static readonly byte[] Limit = new byte[5 * (255 + 1)]; + private static readonly byte[] Limit = new byte[5 * (MaxJSample + 1)]; static IDCT() { - // First segment of range limit table: limit[x] = 0 for x < 0 - // allow negative subscripts of simple table */ - int tableOffset = 2 * (255 + 1); - // Main part of range limit table: limit[x] = x int i; - for (i = 0; i <= 255; i++) + for (i = 0; i <= MaxJSample; i++) { - Limit[tableOffset + i] = (byte)i; + Limit[TableOffset + i] = (byte)i; } - /* End of range limit table: limit[x] = MAXJSAMPLE for x > MAXJSAMPLE */ - for (; i < 3 * (255 + 1); i++) + // End of range limit table: limit[x] = MaxJSample for x > MaxJSample + for (; i < 3 * (MaxJSample + 1); i++) { - Limit[tableOffset + i] = 255; + Limit[TableOffset + i] = MaxJSample; } } @@ -183,7 +198,7 @@ t = ((DctSqrt2 * p0) + 8192) >> 14; // convert to 8 bit - t = (t < -2040) ? 0 : (t >= 2024) ? 255 : (t + 2056) >> 4; + t = (t < -2040) ? 0 : (t >= 2024) ? MaxJSample : (t + 2056) >> 4; short st = (short)t; blockData[col] = st; @@ -243,14 +258,14 @@ p4 = v3 - v4; // convert to 8-bit integers - p0 = (p0 < 16) ? 0 : (p0 >= 4080) ? 255 : p0 >> 4; - p1 = (p1 < 16) ? 0 : (p1 >= 4080) ? 255 : p1 >> 4; - p2 = (p2 < 16) ? 0 : (p2 >= 4080) ? 255 : p2 >> 4; - p3 = (p3 < 16) ? 0 : (p3 >= 4080) ? 255 : p3 >> 4; - p4 = (p4 < 16) ? 0 : (p4 >= 4080) ? 255 : p4 >> 4; - p5 = (p5 < 16) ? 0 : (p5 >= 4080) ? 255 : p5 >> 4; - p6 = (p6 < 16) ? 0 : (p6 >= 4080) ? 255 : p6 >> 4; - p7 = (p7 < 16) ? 0 : (p7 >= 4080) ? 255 : p7 >> 4; + p0 = (p0 < 16) ? 0 : (p0 >= 4080) ? MaxJSample : p0 >> 4; + p1 = (p1 < 16) ? 0 : (p1 >= 4080) ? MaxJSample : p1 >> 4; + p2 = (p2 < 16) ? 0 : (p2 >= 4080) ? MaxJSample : p2 >> 4; + p3 = (p3 < 16) ? 0 : (p3 >= 4080) ? MaxJSample : p3 >> 4; + p4 = (p4 < 16) ? 0 : (p4 >= 4080) ? MaxJSample : p4 >> 4; + p5 = (p5 < 16) ? 0 : (p5 >= 4080) ? MaxJSample : p5 >> 4; + p6 = (p6 < 16) ? 0 : (p6 >= 4080) ? MaxJSample : p6 >> 4; + p7 = (p7 < 16) ? 0 : (p7 >= 4080) ? MaxJSample : p7 >> 4; // store block data blockData[col] = (short)p0; @@ -266,7 +281,6 @@ /// /// A port of - /// TODO: This does not work!! /// A 2-D IDCT can be done by 1-D IDCT on each column followed by 1-D IDCT /// on each row(or vice versa, but it's more convenient to emit a row at /// a time). Direct algorithms are also available, but they are much more @@ -274,7 +288,7 @@ /// /// This implementation is based on Arai, Agui, and Nakajima's algorithm for /// scaled DCT.Their original paper (Trans.IEICE E-71(11):1095) is in - /// Japanese, but the algorithm is described in the Pennebaker & Mitchell + /// Japanese, but the algorithm is described in the Pennebaker & Mitchell /// JPEG textbook(see REFERENCES section in file README.ijg). The following /// code is based directly on figure 4-8 in P&M. /// While an 8-point DCT cannot be done in less than 11 multiplies, it is @@ -293,177 +307,228 @@ /// The fram component /// The block buffer offset /// The computational buffer for holding temp values - public static void QuantizeAndInverseAlt(QuantizationTables quantizationTables, ref FrameComponent component, int blockBufferOffset, Buffer computationBuffer) + public static void QuantizeAndInverseAlt( + QuantizationTables quantizationTables, + ref FrameComponent component, + int blockBufferOffset, + Buffer computationBuffer) { Span qt = quantizationTables.Tables.GetRowSpan(component.QuantizationIdentifier); Span blockData = component.BlockData.Slice(blockBufferOffset); Span computationBufferSpan = computationBuffer; - int p0, p1, p2, p3, p4, p5, p6, p7; - - for (int col = 0; col < 8; col++) + // For AA&N IDCT method, multiplier are equal to quantization + // coefficients scaled by scalefactor[row]*scalefactor[col], where + // scalefactor[0] = 1 + // scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7 + // For integer operation, the multiplier table is to be scaled by 14. + using (var multiplier = new Buffer(64)) { - // Gather block data - p0 = blockData[col]; - p1 = blockData[col + 8]; - p2 = blockData[col + 16]; - p3 = blockData[col + 24]; - p4 = blockData[col + 32]; - p5 = blockData[col + 40]; - p6 = blockData[col + 48]; - p7 = blockData[col + 56]; - - int tmp0 = p0 * qt[col]; - - // Check for all-zero AC coefficients - if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + Span multiplierSpan = multiplier; + for (int i = 0; i < 64; i++) { - short dcval = (short)tmp0; - - computationBufferSpan[col] = dcval; - computationBufferSpan[col + 8] = dcval; - computationBufferSpan[col + 16] = dcval; - computationBufferSpan[col + 24] = dcval; - computationBufferSpan[col + 32] = dcval; - computationBufferSpan[col + 40] = dcval; - computationBufferSpan[col + 48] = dcval; - computationBufferSpan[col + 56] = dcval; - continue; + multiplierSpan[i] = (short)Descale(qt[i] * Aanscales[i], 14 - Pass1Bits); } - // Even part - int tmp1 = p2 * qt[col + 16]; - int tmp2 = p4 * qt[col + 32]; - int tmp3 = p6 * qt[col + 48]; - - int tmp10 = tmp0 + tmp2; // Phase 3 - int tmp11 = tmp0 - tmp2; - - int tmp13 = tmp1 + tmp3; // Phases 5-3 - int tmp12 = Multiply(tmp1 - tmp3, FIX_1_414213562) - tmp13; // 2*c4 - - tmp0 = tmp10 + tmp13; // Phase 2 - tmp3 = tmp10 - tmp13; - tmp1 = tmp11 + tmp12; - tmp2 = tmp11 - tmp12; - - // Odd Part - int tmp4 = p1 * qt[col + 8]; - int tmp5 = p3 * qt[col + 24]; - int tmp6 = p5 * qt[col + 40]; - int tmp7 = p7 * qt[col + 56]; - - int z13 = tmp6 + tmp5; // Phase 6 - int z10 = tmp6 - tmp5; - int z11 = tmp4 + tmp7; - int z12 = tmp4 - tmp7; - - tmp7 = z11 + z13; // Phase 5 - tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 - - int z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 - tmp10 = Multiply(z12, FIX_1_082392200) - z5; // 2*(c2-c6) - tmp12 = Multiply(z10, FIX_2_613125930) + z5; // 2*(c2+c6) - - tmp6 = tmp12 - tmp7; // Phase 2 - tmp5 = tmp11 - tmp6; - tmp4 = tmp10 - tmp5; - - computationBufferSpan[col] = (short)(tmp0 + tmp7); - computationBufferSpan[col + 56] = (short)(tmp0 - tmp7); - computationBufferSpan[col + 8] = (short)(tmp1 + tmp6); - computationBufferSpan[col + 48] = (short)(tmp1 - tmp6); - computationBufferSpan[col + 16] = (short)(tmp2 + tmp5); - computationBufferSpan[col + 40] = (short)(tmp2 - tmp5); - computationBufferSpan[col + 32] = (short)(tmp3 + tmp4); - computationBufferSpan[col + 24] = (short)(tmp3 - tmp4); - } + int p0, p1, p2, p3, p4, p5, p6, p7; - // Pass 2: process rows from work array, store into output array. - // Note that we must descale the results by a factor of 8 == 2**3, - // and also undo the pass 1 bits scaling. - for (int row = 0; row < 64; row += 8) - { - p0 = computationBufferSpan[row]; - p1 = computationBufferSpan[row + 1]; - p2 = computationBufferSpan[row + 2]; - p3 = computationBufferSpan[row + 3]; - p4 = computationBufferSpan[row + 4]; - p5 = computationBufferSpan[row + 5]; - p6 = computationBufferSpan[row + 6]; - p7 = computationBufferSpan[row + 7]; - - // Check for all-zero AC coefficients - if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + int coefBlockIndex = 0; + int workspaceIndex = 0; + int quantTableIndex = 0; + for (int col = 8; col > 0; col--) { - byte dcval = Limit[Descale(p0, ScaleBits + 3) & RangeMask]; - - blockData[row] = dcval; - blockData[row + 1] = dcval; - blockData[row + 2] = dcval; - blockData[row + 3] = dcval; - blockData[row + 4] = dcval; - blockData[row + 5] = dcval; - blockData[row + 6] = dcval; - blockData[row + 7] = dcval; - - continue; + // Gather block data + p0 = blockData[coefBlockIndex]; + p1 = blockData[coefBlockIndex + 8]; + p2 = blockData[coefBlockIndex + 16]; + p3 = blockData[coefBlockIndex + 24]; + p4 = blockData[coefBlockIndex + 32]; + p5 = blockData[coefBlockIndex + 40]; + p6 = blockData[coefBlockIndex + 48]; + p7 = blockData[coefBlockIndex + 56]; + + int tmp0 = p0 * multiplierSpan[quantTableIndex]; + + // Due to quantization, we will usually find that many of the input + // coefficients are zero, especially the AC terms. We can exploit this + // by short-circuiting the IDCT calculation for any column in which all + // the AC terms are zero. In that case each output is equal to the + // DC coefficient (with scale factor as needed). + // With typical images and quantization tables, half or more of the + // column DCT calculations can be simplified this way. + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + short dcval = (short)tmp0; + + computationBufferSpan[workspaceIndex] = dcval; + computationBufferSpan[workspaceIndex + 8] = dcval; + computationBufferSpan[workspaceIndex + 16] = dcval; + computationBufferSpan[workspaceIndex + 24] = dcval; + computationBufferSpan[workspaceIndex + 32] = dcval; + computationBufferSpan[workspaceIndex + 40] = dcval; + computationBufferSpan[workspaceIndex + 48] = dcval; + computationBufferSpan[workspaceIndex + 56] = dcval; + + coefBlockIndex++; + quantTableIndex++; + workspaceIndex++; + continue; + } + + // Even part + int tmp1 = p2 * multiplierSpan[quantTableIndex + 16]; + int tmp2 = p4 * multiplierSpan[quantTableIndex + 32]; + int tmp3 = p6 * multiplierSpan[quantTableIndex + 48]; + + int tmp10 = tmp0 + tmp2; // Phase 3 + int tmp11 = tmp0 - tmp2; + + int tmp13 = tmp1 + tmp3; // Phases 5-3 + int tmp12 = Multiply(tmp1 - tmp3, FIX_1_414213562) - tmp13; // 2*c4 + + tmp0 = tmp10 + tmp13; // Phase 2 + tmp3 = tmp10 - tmp13; + tmp1 = tmp11 + tmp12; + tmp2 = tmp11 - tmp12; + + // Odd Part + int tmp4 = p1 * multiplierSpan[quantTableIndex + 8]; + int tmp5 = p3 * multiplierSpan[quantTableIndex + 24]; + int tmp6 = p5 * multiplierSpan[quantTableIndex + 40]; + int tmp7 = p7 * multiplierSpan[quantTableIndex + 56]; + + int z13 = tmp6 + tmp5; // Phase 6 + int z10 = tmp6 - tmp5; + int z11 = tmp4 + tmp7; + int z12 = tmp4 - tmp7; + + tmp7 = z11 + z13; // Phase 5 + tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 + + int z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 + tmp10 = z5 - Multiply(z12, FIX_1_082392200); // 2*(c2-c6) + tmp12 = z5 - Multiply(z10, FIX_2_613125930); // 2*(c2+c6) + + tmp6 = tmp12 - tmp7; // Phase 2 + tmp5 = tmp11 - tmp6; + tmp4 = tmp10 - tmp5; + + computationBufferSpan[workspaceIndex] = (short)(tmp0 + tmp7); + computationBufferSpan[workspaceIndex + 56] = (short)(tmp0 - tmp7); + computationBufferSpan[workspaceIndex + 8] = (short)(tmp1 + tmp6); + computationBufferSpan[workspaceIndex + 48] = (short)(tmp1 - tmp6); + computationBufferSpan[workspaceIndex + 16] = (short)(tmp2 + tmp5); + computationBufferSpan[workspaceIndex + 40] = (short)(tmp2 - tmp5); + computationBufferSpan[workspaceIndex + 24] = (short)(tmp3 + tmp4); + computationBufferSpan[workspaceIndex + 32] = (short)(tmp3 - tmp4); + + coefBlockIndex++; + quantTableIndex++; + workspaceIndex++; } - // Even part - int tmp10 = p0 + p4; - int tmp11 = p0 - p4; - - int tmp13 = p2 + p6; - int tmp12 = Multiply(p2 - p6, FIX_1_414213562) - tmp13; /* 2*c4 */ - - int tmp0 = tmp10 + tmp13; - int tmp3 = tmp10 - tmp13; - int tmp1 = tmp11 + tmp12; - int tmp2 = tmp11 - tmp12; - - // Odd part - int z13 = p5 + p3; - int z10 = p5 - p3; - int z11 = p1 + p7; - int z12 = p1 - p7; - - int tmp7 = z11 + z13; // Phase 5 - tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 - - int z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 - tmp10 = Multiply(z12, FIX_1_082392200) - z5; // 2*(c2-c6) - tmp12 = Multiply(z10, FIX_2_613125930) + z5; // -2*(c2+c6) - - int tmp6 = tmp12 - tmp7; // Phase 2 - int tmp5 = tmp11 - tmp6; - int tmp4 = tmp10 - tmp5; - - // Final output stage: scale down by a factor of 8 and range-limit - blockData[row] = Limit[Descale(tmp0 + tmp7, ScaleBits + 3) & RangeMask]; - blockData[row + 7] = Limit[Descale(tmp0 - tmp7, ScaleBits + 3) & RangeMask]; - blockData[row + 1] = Limit[Descale(tmp1 + tmp6, ScaleBits + 3) & RangeMask]; - blockData[row + 6] = Limit[Descale(tmp1 - tmp6, ScaleBits + 3) & RangeMask]; - blockData[row + 2] = Limit[Descale(tmp2 + tmp5, ScaleBits + 3) & RangeMask]; - blockData[row + 5] = Limit[Descale(tmp2 - tmp5, ScaleBits + 3) & RangeMask]; - blockData[row + 3] = Limit[Descale(tmp3 + tmp4, ScaleBits + 3) & RangeMask]; - blockData[row + 4] = Limit[Descale(tmp3 - tmp4, ScaleBits + 3) & RangeMask]; + // Pass 2: process rows from work array, store into output array. + // Note that we must descale the results by a factor of 8 == 2**3, + // and also undo the pass 1 bits scaling. + for (int row = 0; row < 64; row += 8) + { + p1 = computationBufferSpan[row + 1]; + p2 = computationBufferSpan[row + 2]; + p3 = computationBufferSpan[row + 3]; + p4 = computationBufferSpan[row + 4]; + p5 = computationBufferSpan[row + 5]; + p6 = computationBufferSpan[row + 6]; + p7 = computationBufferSpan[row + 7]; + + // Add range center and fudge factor for final descale and range-limit. + int z5 = computationBufferSpan[row] + (RangeCenter << (Pass1Bits + 3)) + (1 << (Pass1Bits + 2)); + + // Check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) + { + byte dcval = DoLimit(RightShift(z5, Pass1Bits + 3) & RangeMask); + + blockData[row] = dcval; + blockData[row + 1] = dcval; + blockData[row + 2] = dcval; + blockData[row + 3] = dcval; + blockData[row + 4] = dcval; + blockData[row + 5] = dcval; + blockData[row + 6] = dcval; + blockData[row + 7] = dcval; + + continue; + } + + // Even part + int tmp10 = z5 + p4; + int tmp11 = z5 - p4; + + int tmp13 = p2 + p6; + int tmp12 = Multiply(p2 - p6, FIX_1_414213562) - tmp13; /* 2*c4 */ + + int tmp0 = tmp10 + tmp13; + int tmp3 = tmp10 - tmp13; + int tmp1 = tmp11 + tmp12; + int tmp2 = tmp11 - tmp12; + + // Odd part + int z13 = p5 + p3; + int z10 = p5 - p3; + int z11 = p1 + p7; + int z12 = p1 - p7; + + int tmp7 = z11 + z13; // Phase 5 + tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 + + z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 + tmp10 = z5 - Multiply(z12, FIX_1_082392200); // 2*(c2-c6) + tmp12 = z5 - Multiply(z10, FIX_2_613125930); // 2*(c2+c6) + + int tmp6 = tmp12 - tmp7; // Phase 2 + int tmp5 = tmp11 - tmp6; + int tmp4 = tmp10 - tmp5; + + // Final output stage: scale down by a factor of 8 and range-limit + blockData[row] = DoLimit(Descale(tmp0 + tmp7, Pass1Bits + 3) & RangeMask); + blockData[row + 7] = DoLimit(Descale(tmp0 - tmp7, Pass1Bits + 3) & RangeMask); + blockData[row + 1] = DoLimit(Descale(tmp1 + tmp6, Pass1Bits + 3) & RangeMask); + blockData[row + 6] = DoLimit(Descale(tmp1 - tmp6, Pass1Bits + 3) & RangeMask); + blockData[row + 2] = DoLimit(Descale(tmp2 + tmp5, Pass1Bits + 3) & RangeMask); + blockData[row + 5] = DoLimit(Descale(tmp2 - tmp5, Pass1Bits + 3) & RangeMask); + blockData[row + 3] = DoLimit(Descale(tmp3 + tmp4, Pass1Bits + 3) & RangeMask); + blockData[row + 4] = DoLimit(Descale(tmp3 - tmp4, Pass1Bits + 3) & RangeMask); + } } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Multiply(int val, int c) { - return Descale(val * c, 8); + return Descale(val * c, ConstBits); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RightShift(int x, int shft) { return x >> shft; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Descale(int x, int n) { return RightShift(x + (1 << (n - 1)), n); } + + /// + /// Offsets the value by 128 and limits it to the 0..255 range + /// + /// The value + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte DoLimit(int value) + { + return Limit[value + LimitOffset]; + } } } \ No newline at end of file From 8c22c11408feccd56663ff60036da693c04cb9bf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 9 Jul 2017 11:21:02 +1000 Subject: [PATCH 35/43] A little cleanup --- .../Formats/Jpeg/Port/Components/IDCT.cs | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs index 22d787269..cae1e8434 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -1,7 +1,6 @@ namespace ImageSharp.Formats.Jpeg.Port.Components { using System; - using System.Numerics; using System.Runtime.CompilerServices; using ImageSharp.Memory; @@ -69,7 +68,7 @@ Limit[TableOffset + i] = (byte)i; } - // End of range limit table: limit[x] = MaxJSample for x > MaxJSample + // End of range limit table: Limit[x] = MaxJSample for x > MaxJSample for (; i < 3 * (MaxJSample + 1); i++) { Limit[TableOffset + i] = MaxJSample; @@ -446,7 +445,7 @@ // Check for all-zero AC coefficients if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) { - byte dcval = DoLimit(RightShift(z5, Pass1Bits + 3) & RangeMask); + byte dcval = Limit[LimitOffset + (RightShift(z5, Pass1Bits + 3) & RangeMask)]; blockData[row] = dcval; blockData[row + 1] = dcval; @@ -489,46 +488,55 @@ int tmp5 = tmp11 - tmp6; int tmp4 = tmp10 - tmp5; - // Final output stage: scale down by a factor of 8 and range-limit - blockData[row] = DoLimit(Descale(tmp0 + tmp7, Pass1Bits + 3) & RangeMask); - blockData[row + 7] = DoLimit(Descale(tmp0 - tmp7, Pass1Bits + 3) & RangeMask); - blockData[row + 1] = DoLimit(Descale(tmp1 + tmp6, Pass1Bits + 3) & RangeMask); - blockData[row + 6] = DoLimit(Descale(tmp1 - tmp6, Pass1Bits + 3) & RangeMask); - blockData[row + 2] = DoLimit(Descale(tmp2 + tmp5, Pass1Bits + 3) & RangeMask); - blockData[row + 5] = DoLimit(Descale(tmp2 - tmp5, Pass1Bits + 3) & RangeMask); - blockData[row + 3] = DoLimit(Descale(tmp3 + tmp4, Pass1Bits + 3) & RangeMask); - blockData[row + 4] = DoLimit(Descale(tmp3 - tmp4, Pass1Bits + 3) & RangeMask); + // Final output stage: scale down by a factor of 8, offset, and range-limit + blockData[row] = Limit[LimitOffset + (RightShift(tmp0 + tmp7, Pass1Bits + 3) & RangeMask)]; + blockData[row + 7] = Limit[LimitOffset + (RightShift(tmp0 - tmp7, Pass1Bits + 3) & RangeMask)]; + blockData[row + 1] = Limit[LimitOffset + (RightShift(tmp1 + tmp6, Pass1Bits + 3) & RangeMask)]; + blockData[row + 6] = Limit[LimitOffset + (RightShift(tmp1 - tmp6, Pass1Bits + 3) & RangeMask)]; + blockData[row + 2] = Limit[LimitOffset + (RightShift(tmp2 + tmp5, Pass1Bits + 3) & RangeMask)]; + blockData[row + 5] = Limit[LimitOffset + (RightShift(tmp2 - tmp5, Pass1Bits + 3) & RangeMask)]; + blockData[row + 3] = Limit[LimitOffset + (RightShift(tmp3 + tmp4, Pass1Bits + 3) & RangeMask)]; + blockData[row + 4] = Limit[LimitOffset + (RightShift(tmp3 - tmp4, Pass1Bits + 3) & RangeMask)]; } } } + /// + /// Multiply a variable by an int constant, and immediately descale. + /// + /// The value + /// The multiplier + /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Multiply(int val, int c) { return Descale(val * c, ConstBits); } + /// + /// Right-shifts the value by the given amount + /// + /// The value + /// The amount to shift by + /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int RightShift(int x, int shft) - { - return x >> shft; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Descale(int x, int n) + private static int RightShift(int value, int shift) { - return RightShift(x + (1 << (n - 1)), n); + return value >> shift; } /// - /// Offsets the value by 128 and limits it to the 0..255 range + /// Descale and correctly round an int value that's scaled by bits. + /// We assume rounds towards minus infinity, so adding + /// the fudge factor is correct for either sign of . /// /// The value - /// The + /// The number of bits + /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static byte DoLimit(int value) + private static int Descale(int value, int n) { - return Limit[value + LimitOffset]; + return RightShift(value + (1 << (n - 1)), n); } } } \ No newline at end of file From a236a80666b6e236308cd28cbc85c00a69208d7f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 9 Jul 2017 11:35:04 +1000 Subject: [PATCH 36/43] Simplify indexing TODO: Allocations! --- .../Formats/Jpeg/Port/Components/IDCT.cs | 80 ++++++++----------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs index cae1e8434..65c2bbde6 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -331,22 +331,19 @@ int p0, p1, p2, p3, p4, p5, p6, p7; - int coefBlockIndex = 0; - int workspaceIndex = 0; - int quantTableIndex = 0; - for (int col = 8; col > 0; col--) + for (int col = 0; col < 8; col++) { // Gather block data - p0 = blockData[coefBlockIndex]; - p1 = blockData[coefBlockIndex + 8]; - p2 = blockData[coefBlockIndex + 16]; - p3 = blockData[coefBlockIndex + 24]; - p4 = blockData[coefBlockIndex + 32]; - p5 = blockData[coefBlockIndex + 40]; - p6 = blockData[coefBlockIndex + 48]; - p7 = blockData[coefBlockIndex + 56]; + p0 = blockData[col]; + p1 = blockData[col + 8]; + p2 = blockData[col + 16]; + p3 = blockData[col + 24]; + p4 = blockData[col + 32]; + p5 = blockData[col + 40]; + p6 = blockData[col + 48]; + p7 = blockData[col + 56]; - int tmp0 = p0 * multiplierSpan[quantTableIndex]; + int tmp0 = p0 * multiplierSpan[col]; // Due to quantization, we will usually find that many of the input // coefficients are zero, especially the AC terms. We can exploit this @@ -359,25 +356,22 @@ { short dcval = (short)tmp0; - computationBufferSpan[workspaceIndex] = dcval; - computationBufferSpan[workspaceIndex + 8] = dcval; - computationBufferSpan[workspaceIndex + 16] = dcval; - computationBufferSpan[workspaceIndex + 24] = dcval; - computationBufferSpan[workspaceIndex + 32] = dcval; - computationBufferSpan[workspaceIndex + 40] = dcval; - computationBufferSpan[workspaceIndex + 48] = dcval; - computationBufferSpan[workspaceIndex + 56] = dcval; - - coefBlockIndex++; - quantTableIndex++; - workspaceIndex++; + computationBufferSpan[col] = dcval; + computationBufferSpan[col + 8] = dcval; + computationBufferSpan[col + 16] = dcval; + computationBufferSpan[col + 24] = dcval; + computationBufferSpan[col + 32] = dcval; + computationBufferSpan[col + 40] = dcval; + computationBufferSpan[col + 48] = dcval; + computationBufferSpan[col + 56] = dcval; + continue; } // Even part - int tmp1 = p2 * multiplierSpan[quantTableIndex + 16]; - int tmp2 = p4 * multiplierSpan[quantTableIndex + 32]; - int tmp3 = p6 * multiplierSpan[quantTableIndex + 48]; + int tmp1 = p2 * multiplierSpan[col + 16]; + int tmp2 = p4 * multiplierSpan[col + 32]; + int tmp3 = p6 * multiplierSpan[col + 48]; int tmp10 = tmp0 + tmp2; // Phase 3 int tmp11 = tmp0 - tmp2; @@ -391,10 +385,10 @@ tmp2 = tmp11 - tmp12; // Odd Part - int tmp4 = p1 * multiplierSpan[quantTableIndex + 8]; - int tmp5 = p3 * multiplierSpan[quantTableIndex + 24]; - int tmp6 = p5 * multiplierSpan[quantTableIndex + 40]; - int tmp7 = p7 * multiplierSpan[quantTableIndex + 56]; + int tmp4 = p1 * multiplierSpan[col + 8]; + int tmp5 = p3 * multiplierSpan[col + 24]; + int tmp6 = p5 * multiplierSpan[col + 40]; + int tmp7 = p7 * multiplierSpan[col + 56]; int z13 = tmp6 + tmp5; // Phase 6 int z10 = tmp6 - tmp5; @@ -412,18 +406,14 @@ tmp5 = tmp11 - tmp6; tmp4 = tmp10 - tmp5; - computationBufferSpan[workspaceIndex] = (short)(tmp0 + tmp7); - computationBufferSpan[workspaceIndex + 56] = (short)(tmp0 - tmp7); - computationBufferSpan[workspaceIndex + 8] = (short)(tmp1 + tmp6); - computationBufferSpan[workspaceIndex + 48] = (short)(tmp1 - tmp6); - computationBufferSpan[workspaceIndex + 16] = (short)(tmp2 + tmp5); - computationBufferSpan[workspaceIndex + 40] = (short)(tmp2 - tmp5); - computationBufferSpan[workspaceIndex + 24] = (short)(tmp3 + tmp4); - computationBufferSpan[workspaceIndex + 32] = (short)(tmp3 - tmp4); - - coefBlockIndex++; - quantTableIndex++; - workspaceIndex++; + computationBufferSpan[col] = (short)(tmp0 + tmp7); + computationBufferSpan[col + 56] = (short)(tmp0 - tmp7); + computationBufferSpan[col + 8] = (short)(tmp1 + tmp6); + computationBufferSpan[col + 48] = (short)(tmp1 - tmp6); + computationBufferSpan[col + 16] = (short)(tmp2 + tmp5); + computationBufferSpan[col + 40] = (short)(tmp2 - tmp5); + computationBufferSpan[col + 24] = (short)(tmp3 + tmp4); + computationBufferSpan[col + 32] = (short)(tmp3 - tmp4); } // Pass 2: process rows from work array, store into output array. @@ -464,7 +454,7 @@ int tmp11 = z5 - p4; int tmp13 = p2 + p6; - int tmp12 = Multiply(p2 - p6, FIX_1_414213562) - tmp13; /* 2*c4 */ + int tmp12 = Multiply(p2 - p6, FIX_1_414213562) - tmp13; // 2*c4 int tmp0 = tmp10 + tmp13; int tmp3 = tmp10 - tmp13; From 1cd137d8f9132120d21365cb17199b7c52dc08f7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 9 Jul 2017 17:33:26 +1000 Subject: [PATCH 37/43] Reduce allocations --- .../Formats/Jpeg/Port/Components/IDCT.cs | 477 +++++++++--------- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 20 +- 2 files changed, 245 insertions(+), 252 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs index 65c2bbde6..064b3bea3 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/IDCT.cs @@ -6,24 +6,38 @@ using ImageSharp.Memory; /// - /// Performa the invers + /// Performs the inverse Descrete Cosine Transform on each frame component. /// internal static class IDCT { - private const int DctCos1 = 4017; // cos(pi/16) - private const int DctSin1 = 799; // sin(pi/16) - private const int DctCos3 = 3406; // cos(3*pi/16) - private const int DctSin3 = 2276; // sin(3*pi/16) - private const int DctCos6 = 1567; // cos(6*pi/16) - private const int DctSin6 = 3784; // sin(6*pi/16) - private const int DctSqrt2 = 5793; // sqrt(2) + /// + /// Precomputed values scaled up by 14 bits + /// + public static readonly short[] Aanscales = + { + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, 22725, 31521, 29692, 26722, 22725, 17855, + 12299, 6270, 21407, 29692, 27969, 25172, 21407, 16819, 11585, + 5906, 19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315, + 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, 12873, + 17855, 16819, 15137, 12873, 10114, 6967, 3552, 8867, 12299, + 11585, 10426, 8867, 6967, 4799, 2446, 4520, 6270, 5906, 5315, + 4520, 3552, 2446, 1247 + }; + + private const int DctCos1 = 4017; // cos(pi/16) + private const int DctSin1 = 799; // sin(pi/16) + private const int DctCos3 = 3406; // cos(3*pi/16) + private const int DctSin3 = 2276; // sin(3*pi/16) + private const int DctCos6 = 1567; // cos(6*pi/16) + private const int DctSin6 = 3784; // sin(6*pi/16) + private const int DctSqrt2 = 5793; // sqrt(2) private const int DctSqrt1D2 = 2896; // sqrt(2) / 2 #pragma warning disable SA1310 // Field names must not contain underscore - private const int FIX_1_082392200 = 277; /* FIX(1.082392200) */ - private const int FIX_1_414213562 = 362; /* FIX(1.414213562) */ - private const int FIX_1_847759065 = 473; /* FIX(1.847759065) */ - private const int FIX_2_613125930 = 669; /* FIX(2.613125930) */ + private const int FIX_1_082392200 = 277; // FIX(1.082392200) + private const int FIX_1_414213562 = 362; // FIX(1.414213562) + private const int FIX_1_847759065 = 473; // FIX(1.847759065) + private const int FIX_2_613125930 = 669; // FIX(2.613125930) #pragma warning restore SA1310 // Field names must not contain underscore private const int ConstBits = 8; @@ -42,21 +56,9 @@ // be quite far out of range if the input data is corrupt, so a bulletproof // range-limiting step is required. We use a mask-and-table-lookup method // to do the combined operations quickly, assuming that MaxJSample+1 - // is a power of 2. See the comments with prepare_range_limit_table for more info. + // is a power of 2. private const int RangeMask = (MaxJSample * 4) + 3; // 2 bits wider than legal samples - // Precomputed values scaled up by 14 bits - private static readonly short[] Aanscales = - { - 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, 22725, 31521, 29692, 26722, 22725, 17855, - 12299, 6270, 21407, 29692, 27969, 25172, 21407, 16819, 11585, - 5906, 19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315, - 16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520, 12873, - 17855, 16819, 15137, 12873, 10114, 6967, 3552, 8867, 12299, - 11585, 10426, 8867, 6967, 4799, 2446, 4520, 6270, 5906, 5315, - 4520, 3552, 2446, 1247 - }; - private static readonly byte[] Limit = new byte[5 * (MaxJSample + 1)]; static IDCT() @@ -81,15 +83,13 @@ /// 'Practical Fast 1-D DCT Algorithms with 11 Multiplications', /// IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989, 988-991. /// - /// The quantization tables /// The fram component /// The block buffer offset /// The computational buffer for holding temp values - public static void QuantizeAndInverse(QuantizationTables quantizationTables, ref FrameComponent component, int blockBufferOffset, Buffer computationBuffer) + /// The quantization table + public static void QuantizeAndInverse(ref FrameComponent component, int blockBufferOffset, ref Span computationBuffer, ref Span quantizationTable) { - Span qt = quantizationTables.Tables.GetRowSpan(component.QuantizationIdentifier); Span blockData = component.BlockData.Slice(blockBufferOffset); - Span computationBufferSpan = computationBuffer; int v0, v1, v2, v3, v4, v5, v6, v7; int p0, p1, p2, p3, p4, p5, p6, p7; int t; @@ -108,32 +108,32 @@ p7 = blockData[row + 7]; // dequant p0 - p0 *= qt[row]; + p0 *= quantizationTable[row]; // check for all-zero AC coefficients if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) { t = ((DctSqrt2 * p0) + 512) >> 10; short st = (short)t; - computationBufferSpan[row] = st; - computationBufferSpan[row + 1] = st; - computationBufferSpan[row + 2] = st; - computationBufferSpan[row + 3] = st; - computationBufferSpan[row + 4] = st; - computationBufferSpan[row + 5] = st; - computationBufferSpan[row + 6] = st; - computationBufferSpan[row + 7] = st; + computationBuffer[row] = st; + computationBuffer[row + 1] = st; + computationBuffer[row + 2] = st; + computationBuffer[row + 3] = st; + computationBuffer[row + 4] = st; + computationBuffer[row + 5] = st; + computationBuffer[row + 6] = st; + computationBuffer[row + 7] = st; continue; } // dequant p1 ... p7 - p1 *= qt[row + 1]; - p2 *= qt[row + 2]; - p3 *= qt[row + 3]; - p4 *= qt[row + 4]; - p5 *= qt[row + 5]; - p6 *= qt[row + 6]; - p7 *= qt[row + 7]; + p1 *= quantizationTable[row + 1]; + p2 *= quantizationTable[row + 2]; + p3 *= quantizationTable[row + 3]; + p4 *= quantizationTable[row + 4]; + p5 *= quantizationTable[row + 5]; + p6 *= quantizationTable[row + 6]; + p7 *= quantizationTable[row + 7]; // stage 4 v0 = ((DctSqrt2 * p0) + 128) >> 8; @@ -169,27 +169,27 @@ v6 = t; // stage 1 - computationBufferSpan[row] = (short)(v0 + v7); - computationBufferSpan[row + 7] = (short)(v0 - v7); - computationBufferSpan[row + 1] = (short)(v1 + v6); - computationBufferSpan[row + 6] = (short)(v1 - v6); - computationBufferSpan[row + 2] = (short)(v2 + v5); - computationBufferSpan[row + 5] = (short)(v2 - v5); - computationBufferSpan[row + 3] = (short)(v3 + v4); - computationBufferSpan[row + 4] = (short)(v3 - v4); + computationBuffer[row] = (short)(v0 + v7); + computationBuffer[row + 7] = (short)(v0 - v7); + computationBuffer[row + 1] = (short)(v1 + v6); + computationBuffer[row + 6] = (short)(v1 - v6); + computationBuffer[row + 2] = (short)(v2 + v5); + computationBuffer[row + 5] = (short)(v2 - v5); + computationBuffer[row + 3] = (short)(v3 + v4); + computationBuffer[row + 4] = (short)(v3 - v4); } // inverse DCT on columns for (int col = 0; col < 8; ++col) { - p0 = computationBufferSpan[col]; - p1 = computationBufferSpan[col + 8]; - p2 = computationBufferSpan[col + 16]; - p3 = computationBufferSpan[col + 24]; - p4 = computationBufferSpan[col + 32]; - p5 = computationBufferSpan[col + 40]; - p6 = computationBufferSpan[col + 48]; - p7 = computationBufferSpan[col + 56]; + p0 = computationBuffer[col]; + p1 = computationBuffer[col + 8]; + p2 = computationBuffer[col + 16]; + p3 = computationBuffer[col + 24]; + p4 = computationBuffer[col + 32]; + p5 = computationBuffer[col + 40]; + p6 = computationBuffer[col + 48]; + p7 = computationBuffer[col + 56]; // check for all-zero AC coefficients if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) @@ -302,195 +302,188 @@ /// precise the scaled value, so this implementation does worse with high - /// quality - setting files than with low - quality ones. /// - /// The quantization tables - /// The fram component + /// The frame component /// The block buffer offset /// The computational buffer for holding temp values - public static void QuantizeAndInverseAlt( - QuantizationTables quantizationTables, - ref FrameComponent component, - int blockBufferOffset, - Buffer computationBuffer) + /// The multiplier table + public static void QuantizeAndInverseFast(ref FrameComponent component, int blockBufferOffset, ref Span computationBuffer, ref Span multiplierTable) { - Span qt = quantizationTables.Tables.GetRowSpan(component.QuantizationIdentifier); Span blockData = component.BlockData.Slice(blockBufferOffset); - Span computationBufferSpan = computationBuffer; - - // For AA&N IDCT method, multiplier are equal to quantization - // coefficients scaled by scalefactor[row]*scalefactor[col], where - // scalefactor[0] = 1 - // scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7 - // For integer operation, the multiplier table is to be scaled by 14. - using (var multiplier = new Buffer(64)) + int p0, p1, p2, p3, p4, p5, p6, p7; + + for (int col = 0; col < 8; col++) { - Span multiplierSpan = multiplier; - for (int i = 0; i < 64; i++) + // Gather block data + p0 = blockData[col]; + p1 = blockData[col + 8]; + p2 = blockData[col + 16]; + p3 = blockData[col + 24]; + p4 = blockData[col + 32]; + p5 = blockData[col + 40]; + p6 = blockData[col + 48]; + p7 = blockData[col + 56]; + + int tmp0 = p0 * multiplierTable[col]; + + // Due to quantization, we will usually find that many of the input + // coefficients are zero, especially the AC terms. We can exploit this + // by short-circuiting the IDCT calculation for any column in which all + // the AC terms are zero. In that case each output is equal to the + // DC coefficient (with scale factor as needed). + // With typical images and quantization tables, half or more of the + // column DCT calculations can be simplified this way. + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) { - multiplierSpan[i] = (short)Descale(qt[i] * Aanscales[i], 14 - Pass1Bits); - } + short dcval = (short)tmp0; - int p0, p1, p2, p3, p4, p5, p6, p7; + computationBuffer[col] = dcval; + computationBuffer[col + 8] = dcval; + computationBuffer[col + 16] = dcval; + computationBuffer[col + 24] = dcval; + computationBuffer[col + 32] = dcval; + computationBuffer[col + 40] = dcval; + computationBuffer[col + 48] = dcval; + computationBuffer[col + 56] = dcval; - for (int col = 0; col < 8; col++) - { - // Gather block data - p0 = blockData[col]; - p1 = blockData[col + 8]; - p2 = blockData[col + 16]; - p3 = blockData[col + 24]; - p4 = blockData[col + 32]; - p5 = blockData[col + 40]; - p6 = blockData[col + 48]; - p7 = blockData[col + 56]; - - int tmp0 = p0 * multiplierSpan[col]; - - // Due to quantization, we will usually find that many of the input - // coefficients are zero, especially the AC terms. We can exploit this - // by short-circuiting the IDCT calculation for any column in which all - // the AC terms are zero. In that case each output is equal to the - // DC coefficient (with scale factor as needed). - // With typical images and quantization tables, half or more of the - // column DCT calculations can be simplified this way. - if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) - { - short dcval = (short)tmp0; - - computationBufferSpan[col] = dcval; - computationBufferSpan[col + 8] = dcval; - computationBufferSpan[col + 16] = dcval; - computationBufferSpan[col + 24] = dcval; - computationBufferSpan[col + 32] = dcval; - computationBufferSpan[col + 40] = dcval; - computationBufferSpan[col + 48] = dcval; - computationBufferSpan[col + 56] = dcval; - - continue; - } - - // Even part - int tmp1 = p2 * multiplierSpan[col + 16]; - int tmp2 = p4 * multiplierSpan[col + 32]; - int tmp3 = p6 * multiplierSpan[col + 48]; - - int tmp10 = tmp0 + tmp2; // Phase 3 - int tmp11 = tmp0 - tmp2; - - int tmp13 = tmp1 + tmp3; // Phases 5-3 - int tmp12 = Multiply(tmp1 - tmp3, FIX_1_414213562) - tmp13; // 2*c4 - - tmp0 = tmp10 + tmp13; // Phase 2 - tmp3 = tmp10 - tmp13; - tmp1 = tmp11 + tmp12; - tmp2 = tmp11 - tmp12; - - // Odd Part - int tmp4 = p1 * multiplierSpan[col + 8]; - int tmp5 = p3 * multiplierSpan[col + 24]; - int tmp6 = p5 * multiplierSpan[col + 40]; - int tmp7 = p7 * multiplierSpan[col + 56]; - - int z13 = tmp6 + tmp5; // Phase 6 - int z10 = tmp6 - tmp5; - int z11 = tmp4 + tmp7; - int z12 = tmp4 - tmp7; - - tmp7 = z11 + z13; // Phase 5 - tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 - - int z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 - tmp10 = z5 - Multiply(z12, FIX_1_082392200); // 2*(c2-c6) - tmp12 = z5 - Multiply(z10, FIX_2_613125930); // 2*(c2+c6) - - tmp6 = tmp12 - tmp7; // Phase 2 - tmp5 = tmp11 - tmp6; - tmp4 = tmp10 - tmp5; - - computationBufferSpan[col] = (short)(tmp0 + tmp7); - computationBufferSpan[col + 56] = (short)(tmp0 - tmp7); - computationBufferSpan[col + 8] = (short)(tmp1 + tmp6); - computationBufferSpan[col + 48] = (short)(tmp1 - tmp6); - computationBufferSpan[col + 16] = (short)(tmp2 + tmp5); - computationBufferSpan[col + 40] = (short)(tmp2 - tmp5); - computationBufferSpan[col + 24] = (short)(tmp3 + tmp4); - computationBufferSpan[col + 32] = (short)(tmp3 - tmp4); + continue; } - // Pass 2: process rows from work array, store into output array. - // Note that we must descale the results by a factor of 8 == 2**3, - // and also undo the pass 1 bits scaling. - for (int row = 0; row < 64; row += 8) + // Even part + int tmp1 = p2 * multiplierTable[col + 16]; + int tmp2 = p4 * multiplierTable[col + 32]; + int tmp3 = p6 * multiplierTable[col + 48]; + + int tmp10 = tmp0 + tmp2; // Phase 3 + int tmp11 = tmp0 - tmp2; + + int tmp13 = tmp1 + tmp3; // Phases 5-3 + int tmp12 = Multiply(tmp1 - tmp3, FIX_1_414213562) - tmp13; // 2*c4 + + tmp0 = tmp10 + tmp13; // Phase 2 + tmp3 = tmp10 - tmp13; + tmp1 = tmp11 + tmp12; + tmp2 = tmp11 - tmp12; + + // Odd Part + int tmp4 = p1 * multiplierTable[col + 8]; + int tmp5 = p3 * multiplierTable[col + 24]; + int tmp6 = p5 * multiplierTable[col + 40]; + int tmp7 = p7 * multiplierTable[col + 56]; + + int z13 = tmp6 + tmp5; // Phase 6 + int z10 = tmp6 - tmp5; + int z11 = tmp4 + tmp7; + int z12 = tmp4 - tmp7; + + tmp7 = z11 + z13; // Phase 5 + tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 + + int z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 + tmp10 = z5 - Multiply(z12, FIX_1_082392200); // 2*(c2-c6) + tmp12 = z5 - Multiply(z10, FIX_2_613125930); // 2*(c2+c6) + + tmp6 = tmp12 - tmp7; // Phase 2 + tmp5 = tmp11 - tmp6; + tmp4 = tmp10 - tmp5; + + computationBuffer[col] = (short)(tmp0 + tmp7); + computationBuffer[col + 56] = (short)(tmp0 - tmp7); + computationBuffer[col + 8] = (short)(tmp1 + tmp6); + computationBuffer[col + 48] = (short)(tmp1 - tmp6); + computationBuffer[col + 16] = (short)(tmp2 + tmp5); + computationBuffer[col + 40] = (short)(tmp2 - tmp5); + computationBuffer[col + 24] = (short)(tmp3 + tmp4); + computationBuffer[col + 32] = (short)(tmp3 - tmp4); + } + + // Pass 2: process rows from work array, store into output array. + // Note that we must descale the results by a factor of 8 == 2**3, + // and also undo the pass 1 bits scaling. + for (int row = 0; row < 64; row += 8) + { + p1 = computationBuffer[row + 1]; + p2 = computationBuffer[row + 2]; + p3 = computationBuffer[row + 3]; + p4 = computationBuffer[row + 4]; + p5 = computationBuffer[row + 5]; + p6 = computationBuffer[row + 6]; + p7 = computationBuffer[row + 7]; + + // Add range center and fudge factor for final descale and range-limit. + int z5 = computationBuffer[row] + (RangeCenter << (Pass1Bits + 3)) + (1 << (Pass1Bits + 2)); + + // Check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) { - p1 = computationBufferSpan[row + 1]; - p2 = computationBufferSpan[row + 2]; - p3 = computationBufferSpan[row + 3]; - p4 = computationBufferSpan[row + 4]; - p5 = computationBufferSpan[row + 5]; - p6 = computationBufferSpan[row + 6]; - p7 = computationBufferSpan[row + 7]; - - // Add range center and fudge factor for final descale and range-limit. - int z5 = computationBufferSpan[row] + (RangeCenter << (Pass1Bits + 3)) + (1 << (Pass1Bits + 2)); - - // Check for all-zero AC coefficients - if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) == 0) - { - byte dcval = Limit[LimitOffset + (RightShift(z5, Pass1Bits + 3) & RangeMask)]; - - blockData[row] = dcval; - blockData[row + 1] = dcval; - blockData[row + 2] = dcval; - blockData[row + 3] = dcval; - blockData[row + 4] = dcval; - blockData[row + 5] = dcval; - blockData[row + 6] = dcval; - blockData[row + 7] = dcval; - - continue; - } - - // Even part - int tmp10 = z5 + p4; - int tmp11 = z5 - p4; - - int tmp13 = p2 + p6; - int tmp12 = Multiply(p2 - p6, FIX_1_414213562) - tmp13; // 2*c4 - - int tmp0 = tmp10 + tmp13; - int tmp3 = tmp10 - tmp13; - int tmp1 = tmp11 + tmp12; - int tmp2 = tmp11 - tmp12; - - // Odd part - int z13 = p5 + p3; - int z10 = p5 - p3; - int z11 = p1 + p7; - int z12 = p1 - p7; - - int tmp7 = z11 + z13; // Phase 5 - tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 - - z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 - tmp10 = z5 - Multiply(z12, FIX_1_082392200); // 2*(c2-c6) - tmp12 = z5 - Multiply(z10, FIX_2_613125930); // 2*(c2+c6) - - int tmp6 = tmp12 - tmp7; // Phase 2 - int tmp5 = tmp11 - tmp6; - int tmp4 = tmp10 - tmp5; - - // Final output stage: scale down by a factor of 8, offset, and range-limit - blockData[row] = Limit[LimitOffset + (RightShift(tmp0 + tmp7, Pass1Bits + 3) & RangeMask)]; - blockData[row + 7] = Limit[LimitOffset + (RightShift(tmp0 - tmp7, Pass1Bits + 3) & RangeMask)]; - blockData[row + 1] = Limit[LimitOffset + (RightShift(tmp1 + tmp6, Pass1Bits + 3) & RangeMask)]; - blockData[row + 6] = Limit[LimitOffset + (RightShift(tmp1 - tmp6, Pass1Bits + 3) & RangeMask)]; - blockData[row + 2] = Limit[LimitOffset + (RightShift(tmp2 + tmp5, Pass1Bits + 3) & RangeMask)]; - blockData[row + 5] = Limit[LimitOffset + (RightShift(tmp2 - tmp5, Pass1Bits + 3) & RangeMask)]; - blockData[row + 3] = Limit[LimitOffset + (RightShift(tmp3 + tmp4, Pass1Bits + 3) & RangeMask)]; - blockData[row + 4] = Limit[LimitOffset + (RightShift(tmp3 - tmp4, Pass1Bits + 3) & RangeMask)]; + byte dcval = Limit[LimitOffset + (RightShift(z5, Pass1Bits + 3) & RangeMask)]; + + blockData[row] = dcval; + blockData[row + 1] = dcval; + blockData[row + 2] = dcval; + blockData[row + 3] = dcval; + blockData[row + 4] = dcval; + blockData[row + 5] = dcval; + blockData[row + 6] = dcval; + blockData[row + 7] = dcval; + + continue; } + + // Even part + int tmp10 = z5 + p4; + int tmp11 = z5 - p4; + + int tmp13 = p2 + p6; + int tmp12 = Multiply(p2 - p6, FIX_1_414213562) - tmp13; // 2*c4 + + int tmp0 = tmp10 + tmp13; + int tmp3 = tmp10 - tmp13; + int tmp1 = tmp11 + tmp12; + int tmp2 = tmp11 - tmp12; + + // Odd part + int z13 = p5 + p3; + int z10 = p5 - p3; + int z11 = p1 + p7; + int z12 = p1 - p7; + + int tmp7 = z11 + z13; // Phase 5 + tmp11 = Multiply(z11 - z13, FIX_1_414213562); // 2*c4 + + z5 = Multiply(z10 + z12, FIX_1_847759065); // 2*c2 + tmp10 = z5 - Multiply(z12, FIX_1_082392200); // 2*(c2-c6) + tmp12 = z5 - Multiply(z10, FIX_2_613125930); // 2*(c2+c6) + + int tmp6 = tmp12 - tmp7; // Phase 2 + int tmp5 = tmp11 - tmp6; + int tmp4 = tmp10 - tmp5; + + // Final output stage: scale down by a factor of 8, offset, and range-limit + blockData[row] = Limit[LimitOffset + (RightShift(tmp0 + tmp7, Pass1Bits + 3) & RangeMask)]; + blockData[row + 7] = Limit[LimitOffset + (RightShift(tmp0 - tmp7, Pass1Bits + 3) & RangeMask)]; + blockData[row + 1] = Limit[LimitOffset + (RightShift(tmp1 + tmp6, Pass1Bits + 3) & RangeMask)]; + blockData[row + 6] = Limit[LimitOffset + (RightShift(tmp1 - tmp6, Pass1Bits + 3) & RangeMask)]; + blockData[row + 2] = Limit[LimitOffset + (RightShift(tmp2 + tmp5, Pass1Bits + 3) & RangeMask)]; + blockData[row + 5] = Limit[LimitOffset + (RightShift(tmp2 - tmp5, Pass1Bits + 3) & RangeMask)]; + blockData[row + 3] = Limit[LimitOffset + (RightShift(tmp3 + tmp4, Pass1Bits + 3) & RangeMask)]; + blockData[row + 4] = Limit[LimitOffset + (RightShift(tmp3 - tmp4, Pass1Bits + 3) & RangeMask)]; } } + /// + /// Descale and correctly round an int value that's scaled by bits. + /// We assume rounds towards minus infinity, so adding + /// the fudge factor is correct for either sign of . + /// + /// The value + /// The number of bits + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Descale(int value, int n) + { + return RightShift(value + (1 << (n - 1)), n); + } + /// /// Multiply a variable by an int constant, and immediately descale. /// @@ -514,19 +507,5 @@ { return value >> shift; } - - /// - /// Descale and correctly round an int value that's scaled by bits. - /// We assume rounds towards minus infinity, so adding - /// the fudge factor is correct for either sign of . - /// - /// The value - /// The number of bits - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Descale(int value, int n) - { - return RightShift(value + (1 << (n - 1)), n); - } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index ef49dfaf0..074ee3cfd 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -792,13 +792,28 @@ namespace ImageSharp.Formats.Jpeg.Port int blocksPerLine = component.BlocksPerLine; int blocksPerColumn = component.BlocksPerColumn; using (var computationBuffer = Buffer.CreateClean(64)) - { + using (var multiplicationBuffer = Buffer.CreateClean(64)) + { + Span quantizationTable = this.quantizationTables.Tables.GetRowSpan(frameComponent.QuantizationIdentifier); + Span computationBufferSpan = computationBuffer; + + // For AA&N IDCT method, multiplier are equal to quantization + // coefficients scaled by scalefactor[row]*scalefactor[col], where + // scalefactor[0] = 1 + // scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7 + // For integer operation, the multiplier table is to be scaled by 12. + Span multiplierSpan = multiplicationBuffer; + for (int i = 0; i < 64; i++) + { + multiplierSpan[i] = (short)IDCT.Descale(quantizationTable[i] * IDCT.Aanscales[i], 12); + } + for (int blockRow = 0; blockRow < blocksPerColumn; blockRow++) { for (int blockCol = 0; blockCol < blocksPerLine; blockCol++) { int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); - IDCT.QuantizeAndInverseAlt(this.quantizationTables, ref frameComponent, offset, computationBuffer); + IDCT.QuantizeAndInverseFast(ref frameComponent, offset, ref computationBufferSpan, ref multiplierSpan); } } } @@ -808,7 +823,6 @@ namespace ImageSharp.Formats.Jpeg.Port /// /// Builds the huffman tables - /// TODO: This is our bottleneck. We should use a faster algorithm with a LUT. /// /// The tables /// The table index From 0ff77ec6d6ea122bba8931338bc0edd7b884c0eb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 9 Jul 2017 19:48:01 +1000 Subject: [PATCH 38/43] Move check into if statement --- .../Jpeg/Port/Components/ScanDecoder.cs | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 47ec50d71..066dbf3ac 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -622,29 +622,31 @@ namespace ImageSharp.Formats.Jpeg.Port.Components // TODO: Adding this code introduces error into the decoder. // It doesn't appear to speed anything up either. - // if (this.bitsUnRead < 8) - // { - // if (this.bitsCount <= 0) - // { - // code = (short)this.ReadBit(stream); - // this.bitsUnRead += 8; - // } - // if (this.endOfStreamReached || this.unexpectedMarkerReached) - // { - // return -1; - // } - // this.accumulator = (this.accumulator << 8) | this.bitsData; - // int lutIndex = (this.accumulator >> (this.bitsUnRead - 8)) & 0xFF; - // int v = tree.Lookahead[lutIndex]; - // if (v != 0) - // { - // int nb = (v & 0xFF) - 1; - // this.bitsCount -= nb - 1; - // this.bitsUnRead -= nb; - // v = v >> 8; - // return (short)v; - // } - // } + // if (this.bitsUnRead < 8) + // { + // if (this.bitsCount <= 0) + // { + // code = (short)this.ReadBit(stream); + // if (this.endOfStreamReached || this.unexpectedMarkerReached) + // { + // return -1; + // } + // + // this.bitsUnRead += 8; + // } + // + // this.accumulator = (this.accumulator << 8) | this.bitsData; + // int lutIndex = (this.accumulator >> (8 - this.bitsUnRead)) & 0xFF; + // int v = tree.Lookahead[lutIndex]; + // if (v != 0) + // { + // int nb = (v & 0xFF) - 1; + // this.bitsCount -= nb - 1; + // this.bitsUnRead -= nb; + // v = v >> 8; + // return (short)v; + // } + // } if (code == -1) { code = (short)this.ReadBit(stream); From 996da79f8d9642e7331cdb011a3647398f76312d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Jul 2017 22:14:30 +1000 Subject: [PATCH 39/43] Fix tests --- .../Formats/Jpeg/Port/Components/ScanDecoder.cs | 2 ++ src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs | 9 ++------- tests/ImageSharp.Tests/FileTestBase.cs | 2 ++ tests/ImageSharp.Tests/TestFile.cs | 4 ++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs index 066dbf3ac..8b711eaa8 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs @@ -621,6 +621,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components short code = -1; // TODO: Adding this code introduces error into the decoder. + // NOTES # During investigation of the libjpeg implementation it appears that they pull 32bits at a time and operate on those bits + // using 3 methods: FillBits, PeekBits, and ReadBits. We should attempt to do the same. // It doesn't appear to speed anything up either. // if (this.bitsUnRead < 8) // { diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 074ee3cfd..8bc43f0f7 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -542,11 +542,6 @@ namespace ImageSharp.Formats.Jpeg.Port remaining--; int quantizationTableSpec = this.InputStream.ReadByte(); - if (quantizationTableSpec > 3) - { - throw new ImageFormatException($"Bad Tq index value: {quantizationTableSpec}"); - } - switch (quantizationTableSpec >> 4) { case 0: @@ -561,7 +556,7 @@ namespace ImageSharp.Formats.Jpeg.Port this.InputStream.Read(this.temp, 0, 64); remaining -= 64; - Span tableSpan = this.quantizationTables.Tables.GetRowSpan(quantizationTableSpec); + Span tableSpan = this.quantizationTables.Tables.GetRowSpan(quantizationTableSpec & 15); for (int j = 0; j < 64; j++) { tableSpan[QuantizationTables.DctZigZag[j]] = this.temp[j]; @@ -581,7 +576,7 @@ namespace ImageSharp.Formats.Jpeg.Port this.InputStream.Read(this.temp, 0, 128); remaining -= 128; - Span tableSpan = this.quantizationTables.Tables.GetRowSpan(quantizationTableSpec); + Span tableSpan = this.quantizationTables.Tables.GetRowSpan(quantizationTableSpec & 15); for (int j = 0; j < 64; j++) { tableSpan[QuantizationTables.DctZigZag[j]] = (short)((this.temp[2 * j] << 8) | this.temp[(2 * j) + 1]); diff --git a/tests/ImageSharp.Tests/FileTestBase.cs b/tests/ImageSharp.Tests/FileTestBase.cs index 51a1562f5..1c3cb2d5b 100644 --- a/tests/ImageSharp.Tests/FileTestBase.cs +++ b/tests/ImageSharp.Tests/FileTestBase.cs @@ -70,7 +70,9 @@ namespace ImageSharp.Tests // TestFile.Create(TestImages.Jpeg.Baseline.Ycck), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Baseline.Cmyk), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Baseline.Floorplan), // Perf: Enable for local testing only + // TestFile.Create(TestImages.Jpeg.Progressive.Festzug), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Baseline.Bad.MissingEOF), // Perf: Enable for local testing only + // TestFile.Create(TestImages.Jpeg.Baseline.Bad.ExifUndefType), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Progressive.Fb), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Progressive.Progress), // Perf: Enable for local testing only // TestFile.Create(TestImages.Jpeg.Baseline.GammaDalaiLamaGray), // Perf: Enable for local testing only diff --git a/tests/ImageSharp.Tests/TestFile.cs b/tests/ImageSharp.Tests/TestFile.cs index b09aff78e..ffcd0b95b 100644 --- a/tests/ImageSharp.Tests/TestFile.cs +++ b/tests/ImageSharp.Tests/TestFile.cs @@ -116,7 +116,7 @@ namespace ImageSharp.Tests /// public string GetFileNameWithoutExtension(object value) { - return this.FileNameWithoutExtension + "-" + value; + return $"{this.FileNameWithoutExtension}-{value}"; } /// @@ -138,7 +138,7 @@ namespace ImageSharp.Tests /// public Image CreateImage(IImageDecoder decoder) { - return Image.Load(this.image.Configuration, this.Bytes, decoder); + return Image.Load(this.GetImage().Configuration, this.Bytes, decoder); } private Image GetImage() From 327251809b48dee3da72866be54a5d7104ac6d73 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Jul 2017 22:53:07 +1000 Subject: [PATCH 40/43] Fix broken EXIF test --- src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs | 8 ++++---- src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs index a7fd8fd6a..fc77765b1 100644 --- a/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs @@ -5,6 +5,7 @@ namespace ImageSharp { + using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -82,10 +83,9 @@ namespace ImageSharp this.values.Add(new ExifValue(value)); } } - else - { - this.data = other.data; - } + + this.data = new byte[other.data.Length]; + Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length); } /// diff --git a/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs b/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs index 978d5bc24..d96bcb589 100644 --- a/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs @@ -59,7 +59,8 @@ namespace ImageSharp Guard.NotNull(other, nameof(other)); // TODO: Do we need to copy anything else? - this.data = other.data; + this.data = new byte[other.data.Length]; + Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length); } /// From ae6d91a556eedb7ed489254ae558d89fb98d64ef Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Jul 2017 23:17:36 +1000 Subject: [PATCH 41/43] Fix ImageMetaData test --- src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs | 9 ++++++--- src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs index fc77765b1..be4201c72 100644 --- a/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs @@ -84,8 +84,11 @@ namespace ImageSharp } } - this.data = new byte[other.data.Length]; - Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length); + if (other.data != null) + { + this.data = new byte[other.data.Length]; + Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length); + } } /// @@ -131,7 +134,7 @@ namespace ImageSharp return null; } - if (this.data.Length < (this.thumbnailOffset + this.thumbnailLength)) + if (this.data == null || this.data.Length < (this.thumbnailOffset + this.thumbnailLength)) { return null; } diff --git a/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs b/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs index d96bcb589..1393afa62 100644 --- a/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs @@ -59,8 +59,11 @@ namespace ImageSharp Guard.NotNull(other, nameof(other)); // TODO: Do we need to copy anything else? - this.data = new byte[other.data.Length]; - Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length); + if (other.data != null) + { + this.data = new byte[other.data.Length]; + Buffer.BlockCopy(other.data, 0, this.data, 0, other.data.Length); + } } /// From 4d931e60076d14efa33507782c76f203cbc0a53f Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 11 Jul 2017 13:35:18 +0200 Subject: [PATCH 42/43] more benchmarks --- .../General/Vectorization/DivFloat.cs | 54 -------------- .../General/Vectorization/DivUInt32.cs | 54 -------------- .../General/Vectorization/Divide.cs | 69 ++++++++++++++++++ .../General/Vectorization/MulFloat.cs | 67 ------------------ .../General/Vectorization/MulUInt32.cs | 54 -------------- .../General/Vectorization/Multiply.cs | 50 +++++++++++++ .../Vectorization/SIMDBenchmarkBase.cs | 70 +++++++++++++++++++ .../Image/DecodeJpegMultiple.cs | 38 +++++++++- .../Image/MultiImageBenchmarkBase.cs | 2 +- 9 files changed, 226 insertions(+), 232 deletions(-) delete mode 100644 tests/ImageSharp.Benchmarks/General/Vectorization/DivFloat.cs delete mode 100644 tests/ImageSharp.Benchmarks/General/Vectorization/DivUInt32.cs create mode 100644 tests/ImageSharp.Benchmarks/General/Vectorization/Divide.cs delete mode 100644 tests/ImageSharp.Benchmarks/General/Vectorization/MulFloat.cs delete mode 100644 tests/ImageSharp.Benchmarks/General/Vectorization/MulUInt32.cs create mode 100644 tests/ImageSharp.Benchmarks/General/Vectorization/Multiply.cs create mode 100644 tests/ImageSharp.Benchmarks/General/Vectorization/SIMDBenchmarkBase.cs diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/DivFloat.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/DivFloat.cs deleted file mode 100644 index caadaaa34..000000000 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/DivFloat.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace ImageSharp.Benchmarks.General.Vectorization -{ - using System.Numerics; - - using BenchmarkDotNet.Attributes; - - public class DivFloat - { - private float[] input; - - private float[] result; - - [Params(32)] - public int InputSize { get; set; } - - private float testValue; - - [GlobalSetup] - public void Setup() - { - this.input = new float[this.InputSize]; - this.result = new float[this.InputSize]; - this.testValue = 42; - - for (int i = 0; i < this.InputSize; i++) - { - this.input[i] = (uint)i; - } - } - - [Benchmark(Baseline = true)] - public void Standard() - { - float v = this.testValue; - for (int i = 0; i < this.input.Length; i++) - { - this.result[i] = this.input[i] / v; - } - } - - [Benchmark] - public void Simd() - { - Vector v = new Vector(this.testValue); - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a / v; - a.CopyTo(this.result, i); - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/DivUInt32.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/DivUInt32.cs deleted file mode 100644 index 6b3edb192..000000000 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/DivUInt32.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace ImageSharp.Benchmarks.General.Vectorization -{ - using System.Numerics; - - using BenchmarkDotNet.Attributes; - - public class DivUInt32 - { - private uint[] input; - - private uint[] result; - - [Params(32)] - public int InputSize { get; set; } - - private uint testValue; - - [GlobalSetup] - public void Setup() - { - this.input = new uint[this.InputSize]; - this.result = new uint[this.InputSize]; - this.testValue = 42; - - for (int i = 0; i < this.InputSize; i++) - { - this.input[i] = (uint)i; - } - } - - [Benchmark(Baseline = true)] - public void Standard() - { - uint v = this.testValue; - for (int i = 0; i < this.input.Length; i++) - { - this.result[i] = this.input[i] / v; - } - } - - [Benchmark] - public void Simd() - { - Vector v = new Vector(this.testValue); - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a / v; - a.CopyTo(this.result, i); - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/Divide.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/Divide.cs new file mode 100644 index 000000000..b38429557 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/Vectorization/Divide.cs @@ -0,0 +1,69 @@ +namespace ImageSharp.Benchmarks.General.Vectorization +{ + using System; + using System.Numerics; + + using BenchmarkDotNet.Attributes; + + public class DivFloat : SIMDBenchmarkBase.Divide + { + protected override float GetTestValue() => 42; + + [Benchmark(Baseline = true)] + public void Standard() + { + float v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] / v; + } + } + } + + public class Divide : SIMDBenchmarkBase.Divide + { + protected override uint GetTestValue() => 42; + + [Benchmark(Baseline = true)] + public void Standard() + { + uint v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] / v; + } + } + } + + public class DivInt32 : SIMDBenchmarkBase.Divide + { + protected override int GetTestValue() => 42; + + [Benchmark(Baseline = true)] + public void Standard() + { + int v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] / v; + } + } + } + + public class DivInt16 : SIMDBenchmarkBase.Divide + { + protected override short GetTestValue() => 42; + + protected override Vector GetTestVector() => new Vector(new short[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}); + + [Benchmark(Baseline = true)] + public void Standard() + { + short v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = (short)(this.input[i] / v); + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/MulFloat.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/MulFloat.cs deleted file mode 100644 index e5ae49b2a..000000000 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/MulFloat.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace ImageSharp.Benchmarks.General.Vectorization -{ - using System.Numerics; - - using BenchmarkDotNet.Attributes; - - public class MulFloat - { - private float[] input; - - private float[] result; - - [Params(32)] - public int InputSize { get; set; } - - private float testValue; - - [GlobalSetup] - public void Setup() - { - this.input = new float[this.InputSize]; - this.result = new float[this.InputSize]; - this.testValue = 42; - - for (int i = 0; i < this.InputSize; i++) - { - this.input[i] = i; - } - } - - [Benchmark(Baseline = true)] - public void Standard() - { - float v = this.testValue; - for (int i = 0; i < this.input.Length; i++) - { - this.result[i] = this.input[i] * v; - } - } - - [Benchmark] - public void SimdMultiplyByVector() - { - Vector v = new Vector(this.testValue); - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a * v; - a.CopyTo(this.result, i); - } - } - - [Benchmark] - public void SimdMultiplyByScalar() - { - float v = this.testValue; - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a * v; - a.CopyTo(this.result, i); - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/MulUInt32.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/MulUInt32.cs deleted file mode 100644 index 532acc02d..000000000 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/MulUInt32.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace ImageSharp.Benchmarks.General.Vectorization -{ - using System.Numerics; - - using BenchmarkDotNet.Attributes; - - public class MulUInt32 - { - private uint[] input; - - private uint[] result; - - [Params(32)] - public int InputSize { get; set; } - - private uint testValue; - - [GlobalSetup] - public void Setup() - { - this.input = new uint[this.InputSize]; - this.result = new uint[this.InputSize]; - this.testValue = 42; - - for (int i = 0; i < this.InputSize; i++) - { - this.input[i] = (uint)i; - } - } - - [Benchmark(Baseline = true)] - public void Standard() - { - uint v = this.testValue; - for (int i = 0; i < this.input.Length; i++) - { - this.result[i] = this.input[i] * v; - } - } - - [Benchmark] - public void Simd() - { - Vector v = new Vector(this.testValue); - - for (int i = 0; i < this.input.Length; i += Vector.Count) - { - Vector a = new Vector(this.input, i); - a = a * v; - a.CopyTo(this.result, i); - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/Multiply.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/Multiply.cs new file mode 100644 index 000000000..d1b70f21b --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/Vectorization/Multiply.cs @@ -0,0 +1,50 @@ +namespace ImageSharp.Benchmarks.General.Vectorization +{ + using System.Numerics; + using BenchmarkDotNet.Attributes; + + public class MulUInt32 : SIMDBenchmarkBase.Multiply + { + protected override uint GetTestValue() => 42u; + + [Benchmark(Baseline = true)] + public void Standard() + { + uint v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] * v; + } + } + } + + public class MulInt32 : SIMDBenchmarkBase.Multiply + { + [Benchmark(Baseline = true)] + public void Standard() + { + int v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = this.input[i] * v; + } + } + } + + public class MulInt16 : SIMDBenchmarkBase.Multiply + { + protected override short GetTestValue() => 42; + + protected override Vector GetTestVector() => new Vector(new short[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 }); + + [Benchmark(Baseline = true)] + public void Standard() + { + short v = this.testValue; + for (int i = 0; i < this.input.Length; i++) + { + this.result[i] = (short)(this.input[i] * v); + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/SIMDBenchmarkBase.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/SIMDBenchmarkBase.cs new file mode 100644 index 000000000..76987dbd2 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/Vectorization/SIMDBenchmarkBase.cs @@ -0,0 +1,70 @@ +namespace ImageSharp.Benchmarks.General.Vectorization +{ + using System.Numerics; + using System.Runtime.CompilerServices; + + using BenchmarkDotNet.Attributes; + + public abstract class SIMDBenchmarkBase + where T : struct + { + protected T[] input; + + protected T[] result; + + protected T testValue; + + protected Vector testVector; + + protected virtual T GetTestValue() => default(T); + + protected virtual Vector GetTestVector() => new Vector(this.GetTestValue()); + + + [Params(32)] + public int InputSize { get; set; } + + [GlobalSetup] + public virtual void Setup() + { + this.input = new T[this.InputSize]; + this.result = new T[this.InputSize]; + this.testValue = this.GetTestValue(); + this.testVector = this.GetTestVector(); + } + + public abstract class Multiply : SIMDBenchmarkBase + { + [Benchmark] + public void Simd() + { + Vector v = this.testVector; + + for (int i = 0; i < this.input.Length; i += Vector.Count) + { + Vector a = Unsafe.As>(ref this.input[i]); + a = a * v; + Unsafe.As>(ref this.result[i]) = a; + } + } + } + + public abstract class Divide : SIMDBenchmarkBase + { + [Benchmark] + public void Simd() + { + Vector v = this.testVector; + + for (int i = 0; i < this.input.Length; i += Vector.Count) + { + Vector a = Unsafe.As>(ref this.input[i]); + a = a / v; + Unsafe.As>(ref this.result[i]) = a; + } + } + } + + + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Image/DecodeJpegMultiple.cs b/tests/ImageSharp.Benchmarks/Image/DecodeJpegMultiple.cs index 44c90d253..ebcdf972a 100644 --- a/tests/ImageSharp.Benchmarks/Image/DecodeJpegMultiple.cs +++ b/tests/ImageSharp.Benchmarks/Image/DecodeJpegMultiple.cs @@ -6,8 +6,13 @@ namespace ImageSharp.Benchmarks.Image { using System.Collections.Generic; + using System.IO; + using BenchmarkDotNet.Attributes; + using ImageSharp.Formats; + using ImageSharp.PixelFormats; + using CoreImage = ImageSharp.Image; [Config(typeof(Config.Short))] @@ -20,14 +25,23 @@ namespace ImageSharp.Benchmarks.Image protected override IEnumerable SearchPatterns => new[] { "*.jpg" }; - [Benchmark(Description = "DecodeJpegMultiple - ImageSharp")] - public void DecodeJpegImageSharp() + [Benchmark(Description = "DecodeJpegMultiple - ImageSharp NEW")] + public void DecodeJpegImageSharpNwq() { this.ForEachStream( ms => CoreImage.Load(ms) ); } + + [Benchmark(Description = "DecodeJpegMultiple - ImageSharp Original")] + public void DecodeJpegImageSharpOriginal() + { + this.ForEachStream( + ms => CoreImage.Load(ms, new OriginalJpegDecoder()) + ); + } + [Benchmark(Baseline = true, Description = "DecodeJpegMultiple - System.Drawing")] public void DecodeJpegSystemDrawing() { @@ -36,5 +50,25 @@ namespace ImageSharp.Benchmarks.Image ); } + + public sealed class OriginalJpegDecoder : IImageDecoder, IJpegDecoderOptions + { + /// + /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + public bool IgnoreMetadata { get; set; } + + /// + public Image Decode(Configuration configuration, Stream stream) + where TPixel : struct, IPixel + { + Guard.NotNull(stream, "stream"); + + using (var decoder = new JpegDecoderCore(configuration, this)) + { + return decoder.Decode(stream); + } + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs b/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs index e01a5951e..fcd72a9ba 100644 --- a/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs +++ b/tests/ImageSharp.Benchmarks/Image/MultiImageBenchmarkBase.cs @@ -39,7 +39,7 @@ namespace ImageSharp.Benchmarks.Image [Params(InputImageCategory.AllImages, InputImageCategory.SmallImagesOnly, InputImageCategory.LargeImagesOnly)] public virtual InputImageCategory InputCategory { get; set; } - protected virtual string BaseFolder => "../ImageSharp.Tests/TestImages/Formats/"; + protected virtual string BaseFolder => "../../../../../../../../ImageSharp.Tests/TestImages/Formats/"; protected virtual IEnumerable SearchPatterns => new[] { "*.*" }; From 58281ef5891bce3d91d831d5b4ec3e7f140de031 Mon Sep 17 00:00:00 2001 From: JimBobSquarePants Date: Thu, 17 Aug 2017 09:07:40 +1000 Subject: [PATCH 43/43] Use more accurate IDCT for now --- src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs index 8bc43f0f7..3a6e14f59 100644 --- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs @@ -798,17 +798,17 @@ namespace ImageSharp.Formats.Jpeg.Port // scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7 // For integer operation, the multiplier table is to be scaled by 12. Span multiplierSpan = multiplicationBuffer; - for (int i = 0; i < 64; i++) - { - multiplierSpan[i] = (short)IDCT.Descale(quantizationTable[i] * IDCT.Aanscales[i], 12); - } + // for (int i = 0; i < 64; i++) + // { + // multiplierSpan[i] = (short)IDCT.Descale(quantizationTable[i] * IDCT.Aanscales[i], 12); + // } for (int blockRow = 0; blockRow < blocksPerColumn; blockRow++) { for (int blockCol = 0; blockCol < blocksPerLine; blockCol++) { int offset = GetBlockBufferOffset(ref component, blockRow, blockCol); - IDCT.QuantizeAndInverseFast(ref frameComponent, offset, ref computationBufferSpan, ref multiplierSpan); + IDCT.QuantizeAndInverse(ref frameComponent, offset, ref computationBufferSpan, ref quantizationTable); } } }