From 75277daa01ca158d767d52584bef4ad85ecbc78e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 26 Jul 2016 17:05:57 +1000 Subject: [PATCH] Jpeg now generic Former-commit-id: 12923d7bed65f47787f046e0a8d625817ae3ff2f Former-commit-id: 7f64594e18a9e6bad02bf8be8fd9330515b69e0d Former-commit-id: 377629b2cbcb234c6eabdefe58d68151b00d883e --- src/ImageProcessorCore/Bootstrapper.cs | 2 +- .../Colors/Colorspaces/IAlmostEquatable.cs | 29 + .../Colors/Colorspaces/YCbCr.cs | 182 ++++ .../{ => Colors}/PackedVector/Color.cs | 0 .../PackedVector/ColorDefinitions.cs | 0 .../PackedVector/ColorspaceTransforms.cs | 285 +++++++ .../PackedVector/IPackedVector.cs | 0 .../Formats/Bmp/BmpDecoder.cs | 9 +- .../Formats/Bmp/BmpEncoder.cs | 4 +- .../Formats/IImageDecoder.cs | 7 +- src/ImageProcessorCore/Formats/Jpg/Block.cs | 44 + src/ImageProcessorCore/Formats/Jpg/FDCT.cs | 161 ++++ src/ImageProcessorCore/Formats/Jpg/IDCT.cs | 163 ++++ .../Formats/Jpg/JpegDecoder.cs | 139 +++ .../Jpg/JpegDecoderCore.cs.REMOVED.git-id | 1 + .../Formats/Jpg/JpegEncoder.cs | 96 +++ .../Formats/Jpg/JpegEncoderCore.cs | 796 ++++++++++++++++++ .../Formats/Jpg/JpegFormat.cs | 19 + .../Formats/Jpg/JpegSubsample.cs | 25 + src/ImageProcessorCore/Formats/Jpg/README.md | 3 + .../Image/ImageExtensions.cs | 21 +- 21 files changed, 1967 insertions(+), 19 deletions(-) create mode 100644 src/ImageProcessorCore/Colors/Colorspaces/IAlmostEquatable.cs create mode 100644 src/ImageProcessorCore/Colors/Colorspaces/YCbCr.cs rename src/ImageProcessorCore/{ => Colors}/PackedVector/Color.cs (100%) rename src/ImageProcessorCore/{ => Colors}/PackedVector/ColorDefinitions.cs (100%) create mode 100644 src/ImageProcessorCore/Colors/PackedVector/ColorspaceTransforms.cs rename src/ImageProcessorCore/{ => Colors}/PackedVector/IPackedVector.cs (100%) create mode 100644 src/ImageProcessorCore/Formats/Jpg/Block.cs create mode 100644 src/ImageProcessorCore/Formats/Jpg/FDCT.cs create mode 100644 src/ImageProcessorCore/Formats/Jpg/IDCT.cs create mode 100644 src/ImageProcessorCore/Formats/Jpg/JpegDecoder.cs create mode 100644 src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id create mode 100644 src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs create mode 100644 src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs create mode 100644 src/ImageProcessorCore/Formats/Jpg/JpegFormat.cs create mode 100644 src/ImageProcessorCore/Formats/Jpg/JpegSubsample.cs create mode 100644 src/ImageProcessorCore/Formats/Jpg/README.md diff --git a/src/ImageProcessorCore/Bootstrapper.cs b/src/ImageProcessorCore/Bootstrapper.cs index 932bcf434..99fa6f155 100644 --- a/src/ImageProcessorCore/Bootstrapper.cs +++ b/src/ImageProcessorCore/Bootstrapper.cs @@ -39,7 +39,7 @@ namespace ImageProcessorCore this.imageFormats = new List { new BmpFormat(), - //new JpegFormat(), + new JpegFormat(), new PngFormat(), new GifFormat() }; diff --git a/src/ImageProcessorCore/Colors/Colorspaces/IAlmostEquatable.cs b/src/ImageProcessorCore/Colors/Colorspaces/IAlmostEquatable.cs new file mode 100644 index 000000000..4677c3415 --- /dev/null +++ b/src/ImageProcessorCore/Colors/Colorspaces/IAlmostEquatable.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + + /// + /// Defines a generalized method that a value type or class implements to create + /// a type-specific method for determining approximate equality of instances. + /// + /// The type of objects to compare. + /// The object specifying the type to specify precision with. + public interface IAlmostEquatable where TP : struct, IComparable + { + /// + /// Indicates whether the current object is equal to another object of the same type + /// when compared to the specified precision level. + /// + /// An object to compare with this object. + /// The object specifying the level of precision. + /// + /// true if the current object is equal to the other parameter; otherwise, false. + /// + bool AlmostEquals(T other, TP precision); + } +} diff --git a/src/ImageProcessorCore/Colors/Colorspaces/YCbCr.cs b/src/ImageProcessorCore/Colors/Colorspaces/YCbCr.cs new file mode 100644 index 000000000..faba036ad --- /dev/null +++ b/src/ImageProcessorCore/Colors/Colorspaces/YCbCr.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + using System.ComponentModel; + using System.Numerics; + + /// + /// Represents an YCbCr (luminance, chroma, chroma) color conforming to the + /// Full range standard used in digital imaging systems. + /// + /// + public struct YCbCr : IEquatable, IAlmostEquatable + { + /// + /// Represents a that has Y, Cb, and Cr values set to zero. + /// + public static readonly YCbCr Empty = default(YCbCr); + + /// + /// The epsilon for comparing floating point numbers. + /// + private const float Epsilon = 0.001f; + + /// + /// The backing vector for SIMD support. + /// + private Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The y luminance component. + /// The cb chroma component. + /// The cr chroma component. + public YCbCr(float y, float cb, float cr) + : this() + { + this.backingVector = Vector3.Clamp(new Vector3(y, cb, cr), Vector3.Zero, new Vector3(255)); + } + + /// + /// Gets the Y luminance component. + /// A value ranging between 0 and 255. + /// + public float Y => this.backingVector.X; + + /// + /// Gets the Cb chroma component. + /// A value ranging between 0 and 255. + /// + public float Cb => this.backingVector.Y; + + /// + /// Gets the Cr chroma component. + /// A value ranging between 0 and 255. + /// + public float Cr => this.backingVector.Z; + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator YCbCr(Color color) + { + float r = color.R; + float g = color.G; + float b = color.B; + + float y = (float)((0.299 * r) + (0.587 * g) + (0.114 * b)); + float cb = 128 + (float)((-0.168736 * r) - (0.331264 * g) + (0.5 * b)); + float cr = 128 + (float)((0.5 * r) - (0.418688 * g) - (0.081312 * b)); + + return new YCbCr(y, cb, cr); + } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(YCbCr left, YCbCr right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(YCbCr left, YCbCr right) + { + return !left.Equals(right); + } + + /// + public override int GetHashCode() + { + return GetHashCode(this); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "YCbCr [ Empty ]"; + } + + return $"YCbCr [ Y={this.Y:#0.##}, Cb={this.Cb:#0.##}, Cr={this.Cr:#0.##} ]"; + } + + /// + public override bool Equals(object obj) + { + if (obj is YCbCr) + { + return this.Equals((YCbCr)obj); + } + + return false; + } + + /// + public bool Equals(YCbCr other) + { + return this.AlmostEquals(other, Epsilon); + } + + /// + public bool AlmostEquals(YCbCr other, float precision) + { + Vector3 result = Vector3.Abs(this.backingVector - other.backingVector); + + return result.X < precision + && result.Y < precision + && result.Z < precision; + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// The instance of to return the hash code for. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + private static int GetHashCode(YCbCr color) => color.backingVector.GetHashCode(); + } +} diff --git a/src/ImageProcessorCore/PackedVector/Color.cs b/src/ImageProcessorCore/Colors/PackedVector/Color.cs similarity index 100% rename from src/ImageProcessorCore/PackedVector/Color.cs rename to src/ImageProcessorCore/Colors/PackedVector/Color.cs diff --git a/src/ImageProcessorCore/PackedVector/ColorDefinitions.cs b/src/ImageProcessorCore/Colors/PackedVector/ColorDefinitions.cs similarity index 100% rename from src/ImageProcessorCore/PackedVector/ColorDefinitions.cs rename to src/ImageProcessorCore/Colors/PackedVector/ColorDefinitions.cs diff --git a/src/ImageProcessorCore/Colors/PackedVector/ColorspaceTransforms.cs b/src/ImageProcessorCore/Colors/PackedVector/ColorspaceTransforms.cs new file mode 100644 index 000000000..92b06762e --- /dev/null +++ b/src/ImageProcessorCore/Colors/PackedVector/ColorspaceTransforms.cs @@ -0,0 +1,285 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + + /// + /// Packed vector type containing four 8-bit unsigned normalized values ranging from 0 to 255. + /// The color components are stored in red, green, blue, and alpha order. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public partial struct Color + { + ///// + ///// Allows the implicit conversion of an instance of to a + ///// . + ///// + ///// The instance of to convert. + ///// + ///// An instance of . + ///// + //public static implicit operator Color(Bgra32 color) + //{ + // return new Color(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + //} + + ///// + ///// Allows the implicit conversion of an instance of to a + ///// . + ///// + ///// The instance of to convert. + ///// + ///// An instance of . + ///// + //public static implicit operator Color(Cmyk cmykColor) + //{ + // float r = (1 - cmykColor.C) * (1 - cmykColor.K); + // float g = (1 - cmykColor.M) * (1 - cmykColor.K); + // float b = (1 - cmykColor.Y) * (1 - cmykColor.K); + // return new Color(r, g, b); + //} + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// The instance of to convert. + /// + /// An instance of . + /// + public static implicit operator Color(YCbCr color) + { + float y = color.Y; + float cb = color.Cb - 128; + float cr = color.Cr - 128; + + byte r = (byte)(y + (1.402 * cr)).Clamp(0, 255); + byte g = (byte)(y - (0.34414 * cb) - (0.71414 * cr)).Clamp(0, 255); + byte b = (byte)(y + (1.772 * cb)).Clamp(0, 255); + + return new Color(r, g, b, 255); + } + + ///// + ///// Allows the implicit conversion of an instance of to a + ///// . + ///// + ///// The instance of to convert. + ///// + ///// An instance of . + ///// + //public static implicit operator Color(CieXyz color) + //{ + // float x = color.X / 100F; + // float y = color.Y / 100F; + // float z = color.Z / 100F; + + // // Then XYZ to RGB (multiplication by 100 was done above already) + // float r = (x * 3.2406F) + (y * -1.5372F) + (z * -0.4986F); + // float g = (x * -0.9689F) + (y * 1.8758F) + (z * 0.0415F); + // float b = (x * 0.0557F) + (y * -0.2040F) + (z * 1.0570F); + + // return Color.Compress(new Color(r, g, b)); + //} + + ///// + ///// Allows the implicit conversion of an instance of to a + ///// . + ///// + ///// The instance of to convert. + ///// + ///// An instance of . + ///// + //public static implicit operator Color(Hsv color) + //{ + // float s = color.S; + // float v = color.V; + + // if (Math.Abs(s) < Epsilon) + // { + // return new Color(v, v, v, 1); + // } + + // float h = (Math.Abs(color.H - 360) < Epsilon) ? 0 : color.H / 60; + // int i = (int)Math.Truncate(h); + // float f = h - i; + + // float p = v * (1.0f - s); + // float q = v * (1.0f - (s * f)); + // float t = v * (1.0f - (s * (1.0f - f))); + + // float r, g, b; + // switch (i) + // { + // case 0: + // r = v; + // g = t; + // b = p; + // break; + + // case 1: + // r = q; + // g = v; + // b = p; + // break; + + // case 2: + // r = p; + // g = v; + // b = t; + // break; + + // case 3: + // r = p; + // g = q; + // b = v; + // break; + + // case 4: + // r = t; + // g = p; + // b = v; + // break; + + // default: + // r = v; + // g = p; + // b = q; + // break; + // } + + // return new Color(r, g, b); + //} + + ///// + ///// Allows the implicit conversion of an instance of to a + ///// . + ///// + ///// The instance of to convert. + ///// + ///// An instance of . + ///// + //public static implicit operator Color(Hsl color) + //{ + // float rangedH = color.H / 360f; + // float r = 0; + // float g = 0; + // float b = 0; + // float s = color.S; + // float l = color.L; + + // if (Math.Abs(l) > Epsilon) + // { + // if (Math.Abs(s) < Epsilon) + // { + // r = g = b = l; + // } + // else + // { + // float temp2 = (l < 0.5f) ? l * (1f + s) : l + s - (l * s); + // float temp1 = (2f * l) - temp2; + + // r = GetColorComponent(temp1, temp2, rangedH + 0.3333333F); + // g = GetColorComponent(temp1, temp2, rangedH); + // b = GetColorComponent(temp1, temp2, rangedH - 0.3333333F); + // } + // } + + // return new Color(r, g, b); + //} + + ///// + ///// Allows the implicit conversion of an instance of to a + ///// . + ///// + ///// The instance of to convert. + ///// + ///// An instance of . + ///// + //public static implicit operator Color(CieLab cieLabColor) + //{ + // // First convert back to XYZ... + // float y = (cieLabColor.L + 16F) / 116F; + // float x = (cieLabColor.A / 500F) + y; + // float z = y - (cieLabColor.B / 200F); + + // float x3 = x * x * x; + // float y3 = y * y * y; + // float z3 = z * z * z; + + // x = x3 > 0.008856F ? x3 : (x - 0.137931F) / 7.787F; + // y = (cieLabColor.L > 7.999625F) ? y3 : (cieLabColor.L / 903.3F); + // z = (z3 > 0.008856F) ? z3 : (z - 0.137931F) / 7.787F; + + // x *= 0.95047F; + // z *= 1.08883F; + + // // Then XYZ to RGB (multiplication by 100 was done above already) + // float r = (x * 3.2406F) + (y * -1.5372F) + (z * -0.4986F); + // float g = (x * -0.9689F) + (y * 1.8758F) + (z * 0.0415F); + // float b = (x * 0.0557F) + (y * -0.2040F) + (z * 1.0570F); + + // return Color.Compress(new Color(r, g, b)); + //} + + /// + /// Gets the color component from the given values. + /// + /// The first value. + /// The second value. + /// The third value. + /// + /// The . + /// + private static float GetColorComponent(float first, float second, float third) + { + third = MoveIntoRange(third); + if (third < 0.1666667F) + { + return first + ((second - first) * 6.0f * third); + } + + if (third < 0.5) + { + return second; + } + + if (third < 0.6666667F) + { + return first + ((second - first) * (0.6666667F - third) * 6.0f); + } + + return first; + } + + /// + /// Moves the specific value within the acceptable range for + /// conversion. + /// Used for converting colors to this type. + /// + /// The value to shift. + /// + /// The . + /// + private static float MoveIntoRange(float value) + { + if (value < 0.0) + { + value += 1.0f; + } + else if (value > 1.0) + { + value -= 1.0f; + } + + return value; + } + } +} diff --git a/src/ImageProcessorCore/PackedVector/IPackedVector.cs b/src/ImageProcessorCore/Colors/PackedVector/IPackedVector.cs similarity index 100% rename from src/ImageProcessorCore/PackedVector/IPackedVector.cs rename to src/ImageProcessorCore/Colors/PackedVector/IPackedVector.cs diff --git a/src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs b/src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs index e61f049fc..bd05cc7e8 100644 --- a/src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs @@ -69,15 +69,14 @@ namespace ImageProcessorCore.Formats return isBmp; } - /// - /// Decodes the image from the specified stream to the . - /// - /// The to decode to. - /// The containing image data. + /// public void Decode(Image image, Stream stream) where T : IPackedVector where TP : struct { + Guard.NotNull(image, "image"); + Guard.NotNull(stream, "stream"); + new BmpDecoderCore().Decode(image, stream); } } diff --git a/src/ImageProcessorCore/Formats/Bmp/BmpEncoder.cs b/src/ImageProcessorCore/Formats/Bmp/BmpEncoder.cs index 0d9559143..08f73b181 100644 --- a/src/ImageProcessorCore/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageProcessorCore/Formats/Bmp/BmpEncoder.cs @@ -44,8 +44,8 @@ namespace ImageProcessorCore.Formats /// public void Encode(ImageBase image, Stream stream) - where T : IPackedVector - where TP : struct + where T : IPackedVector + where TP : struct { BmpEncoderCore encoder = new BmpEncoderCore(); encoder.Encode(image, stream, this.BitsPerPixel); diff --git a/src/ImageProcessorCore/Formats/IImageDecoder.cs b/src/ImageProcessorCore/Formats/IImageDecoder.cs index 5b270cb6d..8f9050905 100644 --- a/src/ImageProcessorCore/Formats/IImageDecoder.cs +++ b/src/ImageProcessorCore/Formats/IImageDecoder.cs @@ -39,10 +39,11 @@ namespace ImageProcessorCore.Formats bool IsSupportedFileFormat(byte[] header); /// - /// Decodes the image from the specified stream to the . + /// Decodes the image from the specified stream to the . /// - /// The type of pixels contained within the image. - /// The to decode to. + /// The pixel format. + /// The packed format. long, float. + /// The to decode to. /// The containing image data. void Decode(Image image, Stream stream) where T : IPackedVector diff --git a/src/ImageProcessorCore/Formats/Jpg/Block.cs b/src/ImageProcessorCore/Formats/Jpg/Block.cs new file mode 100644 index 000000000..35aa10f18 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/Block.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Represents an 8x8 block of coefficients to transform and encode. + /// + internal class Block + { + /// + /// Gets the size of the block. + /// + public const int BlockSize = 64; + + /// + /// The array of block data. + /// + private readonly int[] data; + + /// + /// Initializes a new instance of the class. + /// + public Block() + { + this.data = new int[BlockSize]; + } + + /// + /// Gets the pixel data at the given block index. + /// + /// The index of the data to return. + /// + /// The . + /// + public int this[int index] + { + get { return this.data[index]; } + set { this.data[index] = value; } + } + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/FDCT.cs b/src/ImageProcessorCore/Formats/Jpg/FDCT.cs new file mode 100644 index 000000000..e51ea6415 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/FDCT.cs @@ -0,0 +1,161 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Performs a fast, forward descrete cosine transform against the given block + /// decomposing it into 64 orthogonal basis signals. + /// + internal class FDCT + { + // Trigonometric constants in 13-bit fixed point format. + // TODO: Rename and describe these. + private const int fix_0_298631336 = 2446; + private const int fix_0_390180644 = 3196; + private const int fix_0_541196100 = 4433; + private const int fix_0_765366865 = 6270; + private const int fix_0_899976223 = 7373; + private const int fix_1_175875602 = 9633; + private const int fix_1_501321110 = 12299; + private const int fix_1_847759065 = 15137; + private const int fix_1_961570560 = 16069; + private const int fix_2_053119869 = 16819; + private const int fix_2_562915447 = 20995; + private const int fix_3_072711026 = 25172; + + /// + /// The number of bits + /// + private const int Bits = 13; + + /// + /// The number of bits to shift by on the first pass. + /// + private const int Pass1Bits = 2; + + /// + /// The value to shift by + /// + private const int CenterJSample = 128; + + /// + /// Performs a forward DCT on an 8x8 block of coefficients, including a + /// level shift. + /// + /// The block. + public static void Transform(Block block) + { + // Pass 1: process rows. + for (int y = 0; y < 8; y++) + { + int y8 = y * 8; + + int x0 = block[y8]; + int x1 = block[y8 + 1]; + int x2 = block[y8 + 2]; + int x3 = block[y8 + 3]; + int x4 = block[y8 + 4]; + int x5 = block[y8 + 5]; + int x6 = block[y8 + 6]; + int x7 = block[y8 + 7]; + + int tmp0 = x0 + x7; + int tmp1 = x1 + x6; + int tmp2 = x2 + x5; + int tmp3 = x3 + x4; + + int tmp10 = tmp0 + tmp3; + int tmp12 = tmp0 - tmp3; + int tmp11 = tmp1 + tmp2; + int tmp13 = tmp1 - tmp2; + + tmp0 = x0 - x7; + tmp1 = x1 - x6; + tmp2 = x2 - x5; + tmp3 = x3 - x4; + + block[y8] = (tmp10 + tmp11 - (8 * CenterJSample)) << Pass1Bits; + block[y8 + 4] = (tmp10 - tmp11) << Pass1Bits; + int z1 = (tmp12 + tmp13) * fix_0_541196100; + z1 += 1 << (Bits - Pass1Bits - 1); + block[y8 + 2] = (z1 + (tmp12 * fix_0_765366865)) >> (Bits - Pass1Bits); + block[y8 + 6] = (z1 - (tmp13 * fix_1_847759065)) >> (Bits - Pass1Bits); + + tmp10 = tmp0 + tmp3; + tmp11 = tmp1 + tmp2; + tmp12 = tmp0 + tmp2; + tmp13 = tmp1 + tmp3; + z1 = (tmp12 + tmp13) * fix_1_175875602; + z1 += 1 << (Bits - Pass1Bits - 1); + tmp0 = tmp0 * fix_1_501321110; + tmp1 = tmp1 * fix_3_072711026; + tmp2 = tmp2 * fix_2_053119869; + tmp3 = tmp3 * fix_0_298631336; + tmp10 = tmp10 * -fix_0_899976223; + tmp11 = tmp11 * -fix_2_562915447; + tmp12 = tmp12 * -fix_0_390180644; + tmp13 = tmp13 * -fix_1_961570560; + + tmp12 += z1; + tmp13 += z1; + block[y8 + 1] = (tmp0 + tmp10 + tmp12) >> (Bits - Pass1Bits); + block[y8 + 3] = (tmp1 + tmp11 + tmp13) >> (Bits - Pass1Bits); + block[y8 + 5] = (tmp2 + tmp11 + tmp12) >> (Bits - Pass1Bits); + block[y8 + 7] = (tmp3 + tmp10 + tmp13) >> (Bits - Pass1Bits); + } + + // Pass 2: process columns. + // We remove pass1Bits scaling, but leave results scaled up by an overall factor of 8. + for (int x = 0; x < 8; x++) + { + int tmp0 = block[x] + block[56 + x]; + int tmp1 = block[8 + x] + block[48 + x]; + int tmp2 = block[16 + x] + block[40 + x]; + int tmp3 = block[24 + x] + block[32 + x]; + + int tmp10 = tmp0 + tmp3 + (1 << (Pass1Bits - 1)); + int tmp12 = tmp0 - tmp3; + int tmp11 = tmp1 + tmp2; + int tmp13 = tmp1 - tmp2; + + tmp0 = block[x] - block[56 + x]; + tmp1 = block[8 + x] - block[48 + x]; + tmp2 = block[16 + x] - block[40 + x]; + tmp3 = block[24 + x] - block[32 + x]; + + block[x] = (tmp10 + tmp11) >> Pass1Bits; + block[32 + x] = (tmp10 - tmp11) >> Pass1Bits; + + int z1 = (tmp12 + tmp13) * fix_0_541196100; + z1 += 1 << (Bits + Pass1Bits - 1); + block[16 + x] = (z1 + (tmp12 * fix_0_765366865)) >> (Bits + Pass1Bits); + block[48 + x] = (z1 - (tmp13 * fix_1_847759065)) >> (Bits + Pass1Bits); + + tmp10 = tmp0 + tmp3; + tmp11 = tmp1 + tmp2; + tmp12 = tmp0 + tmp2; + tmp13 = tmp1 + tmp3; + z1 = (tmp12 + tmp13) * fix_1_175875602; + z1 += 1 << (Bits + Pass1Bits - 1); + tmp0 = tmp0 * fix_1_501321110; + tmp1 = tmp1 * fix_3_072711026; + tmp2 = tmp2 * fix_2_053119869; + tmp3 = tmp3 * fix_0_298631336; + tmp10 = tmp10 * -fix_0_899976223; + tmp11 = tmp11 * -fix_2_562915447; + tmp12 = tmp12 * -fix_0_390180644; + tmp13 = tmp13 * -fix_1_961570560; + + tmp12 += z1; + tmp13 += z1; + block[8 + x] = (tmp0 + tmp10 + tmp12) >> (Bits + Pass1Bits); + block[24 + x] = (tmp1 + tmp11 + tmp13) >> (Bits + Pass1Bits); + block[40 + x] = (tmp2 + tmp11 + tmp12) >> (Bits + Pass1Bits); + block[56 + x] = (tmp3 + tmp10 + tmp13) >> (Bits + Pass1Bits); + } + } + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/IDCT.cs b/src/ImageProcessorCore/Formats/Jpg/IDCT.cs new file mode 100644 index 000000000..7542f4d38 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/IDCT.cs @@ -0,0 +1,163 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + internal class IDCT + { + private const int w1 = 2841; // 2048*sqrt(2)*cos(1*pi/16) + private const int w2 = 2676; // 2048*sqrt(2)*cos(2*pi/16) + private const int w3 = 2408; // 2048*sqrt(2)*cos(3*pi/16) + private const int w5 = 1609; // 2048*sqrt(2)*cos(5*pi/16) + private const int w6 = 1108; // 2048*sqrt(2)*cos(6*pi/16) + private const int w7 = 565; // 2048*sqrt(2)*cos(7*pi/16) + + private const int w1pw7 = w1 + w7; + private const int w1mw7 = w1 - w7; + private const int w2pw6 = w2 + w6; + private const int w2mw6 = w2 - w6; + private const int w3pw5 = w3 + w5; + private const int w3mw5 = w3 - w5; + + private const int r2 = 181; // 256/sqrt(2) + + // idct performs a 2-D Inverse Discrete Cosine Transformation. + // + // The input coefficients should already have been multiplied by the + // appropriate quantization table. We use fixed-point computation, with the + // number of bits for the fractional component varying over the intermediate + // stages. + // + // For more on the actual algorithm, see Z. Wang, "Fast algorithms for the + // discrete W transform and for the discrete Fourier transform", IEEE Trans. on + // ASSP, Vol. ASSP- 32, pp. 803-816, Aug. 1984. + public static void Transform(Block src) + { + // Horizontal 1-D IDCT. + for (int y = 0; y < 8; y++) + { + int y8 = y * 8; + + // If all the AC components are zero, then the IDCT is trivial. + if (src[y8 + 1] == 0 && src[y8 + 2] == 0 && src[y8 + 3] == 0 && + src[y8 + 4] == 0 && src[y8 + 5] == 0 && src[y8 + 6] == 0 && src[y8 + 7] == 0) + { + int dc = src[y8 + 0] << 3; + src[y8 + 0] = dc; + src[y8 + 1] = dc; + src[y8 + 2] = dc; + src[y8 + 3] = dc; + src[y8 + 4] = dc; + src[y8 + 5] = dc; + src[y8 + 6] = dc; + src[y8 + 7] = dc; + continue; + } + + // Prescale. + int x0 = (src[y8 + 0] << 11) + 128; + int x1 = src[y8 + 4] << 11; + int x2 = src[y8 + 6]; + int x3 = src[y8 + 2]; + int x4 = src[y8 + 1]; + int x5 = src[y8 + 7]; + int x6 = src[y8 + 5]; + int x7 = src[y8 + 3]; + + // Stage 1. + int x8 = w7 * (x4 + x5); + x4 = x8 + w1mw7 * x4; + x5 = x8 - w1pw7 * x5; + x8 = w3 * (x6 + x7); + x6 = x8 - w3mw5 * x6; + x7 = x8 - w3pw5 * x7; + + // Stage 2. + x8 = x0 + x1; + x0 -= x1; + x1 = w6 * (x3 + x2); + x2 = x1 - w2pw6 * x2; + x3 = x1 + w2mw6 * x3; + x1 = x4 + x6; + x4 -= x6; + x6 = x5 + x7; + x5 -= x7; + + // Stage 3. + x7 = x8 + x3; + x8 -= x3; + x3 = x0 + x2; + x0 -= x2; + x2 = (r2 * (x4 + x5) + 128) >> 8; + x4 = (r2 * (x4 - x5) + 128) >> 8; + + // Stage 4. + src[y8 + 0] = (x7 + x1) >> 8; + src[y8 + 1] = (x3 + x2) >> 8; + src[y8 + 2] = (x0 + x4) >> 8; + src[y8 + 3] = (x8 + x6) >> 8; + src[y8 + 4] = (x8 - x6) >> 8; + src[y8 + 5] = (x0 - x4) >> 8; + src[y8 + 6] = (x3 - x2) >> 8; + src[y8 + 7] = (x7 - x1) >> 8; + } + + // Vertical 1-D IDCT. + for (int x = 0; x < 8; x++) + { + // Similar to the horizontal 1-D IDCT case, if all the AC components are zero, then the IDCT is trivial. + // However, after performing the horizontal 1-D IDCT, there are typically non-zero AC components, so + // we do not bother to check for the all-zero case. + + // Prescale. + int y0 = (src[x] << 8) + 8192; + int y1 = src[32 + x] << 8; + int y2 = src[48 + x]; + int y3 = src[16 + x]; + int y4 = src[8 + x]; + int y5 = src[56 + x]; + int y6 = src[40 + x]; + int y7 = src[24 + x]; + + // Stage 1. + int y8 = w7 * (y4 + y5) + 4; + y4 = (y8 + w1mw7 * y4) >> 3; + y5 = (y8 - w1pw7 * y5) >> 3; + y8 = w3 * (y6 + y7) + 4; + y6 = (y8 - w3mw5 * y6) >> 3; + y7 = (y8 - w3pw5 * y7) >> 3; + + // Stage 2. + y8 = y0 + y1; + y0 -= y1; + y1 = w6 * (y3 + y2) + 4; + y2 = (y1 - w2pw6 * y2) >> 3; + y3 = (y1 + w2mw6 * y3) >> 3; + y1 = y4 + y6; + y4 -= y6; + y6 = y5 + y7; + y5 -= y7; + + // Stage 3. + y7 = y8 + y3; + y8 -= y3; + y3 = y0 + y2; + y0 -= y2; + y2 = (r2 * (y4 + y5) + 128) >> 8; + y4 = (r2 * (y4 - y5) + 128) >> 8; + + // Stage 4. + src[x] = (y7 + y1) >> 14; + src[8 + x] = (y3 + y2) >> 14; + src[16 + x] = (y0 + y4) >> 14; + src[24 + x] = (y8 + y6) >> 14; + src[32 + x] = (y8 - y6) >> 14; + src[40 + x] = (y0 - y4) >> 14; + src[48 + x] = (y3 - y2) >> 14; + src[56 + x] = (y7 - y1) >> 14; + } + } + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegDecoder.cs b/src/ImageProcessorCore/Formats/Jpg/JpegDecoder.cs new file mode 100644 index 000000000..84e46d5a4 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/JpegDecoder.cs @@ -0,0 +1,139 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + /// + /// Image decoder for generating an image out of a jpg stream. + /// + public class JpegDecoder : IImageDecoder + { + /// + /// Gets the size of the header for this image type. + /// + /// The size of the header. + public int HeaderSize => 11; + + /// + /// Indicates if the image decoder supports the specified + /// file extension. + /// + /// The file extension. + /// + /// true, if the decoder supports the specified + /// extensions; otherwise false. + /// + /// + /// is null (Nothing in Visual Basic). + /// is a string + /// of length zero or contains only blanks. + public bool IsSupportedFileExtension(string extension) + { + Guard.NotNullOrEmpty(extension, "extension"); + + if (extension.StartsWith(".")) + { + extension = extension.Substring(1); + } + + return extension.Equals("JPG", StringComparison.OrdinalIgnoreCase) || + extension.Equals("JPEG", StringComparison.OrdinalIgnoreCase) || + extension.Equals("JFIF", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Indicates if the image decoder supports the specified + /// file header. + /// + /// The file header. + /// + /// true, if the decoder supports the specified + /// file header; otherwise false. + /// + /// + /// is null (Nothing in Visual Basic). + public bool IsSupportedFileFormat(byte[] header) + { + Guard.NotNull(header, "header"); + + bool isSupported = false; + + if (header.Length >= 11) + { + bool isJfif = IsJfif(header); + bool isExif = IsExif(header); + bool isJpeg = IsJpeg(header); + + isSupported = isJfif || isExif || isJpeg; + } + + return isSupported; + } + + /// + public void Decode(Image image, Stream stream) + where T : IPackedVector + where TP : struct + { + Guard.NotNull(image, "image"); + Guard.NotNull(stream, "stream"); + + JpegDecoderCore decoder = new JpegDecoderCore(); + decoder.Decode(image, stream, false); + } + + /// + /// Returns a value indicating whether the given bytes identify Jfif data. + /// + /// The bytes representing the file header. + /// The + private static bool IsJfif(byte[] header) + { + bool isJfif = + header[6] == 0x4A && // J + header[7] == 0x46 && // F + header[8] == 0x49 && // I + header[9] == 0x46 && // F + header[10] == 0x00; + + return isJfif; + } + + /// + /// Returns a value indicating whether the given bytes identify EXIF data. + /// + /// The bytes representing the file header. + /// The + private static bool IsExif(byte[] header) + { + bool isExif = + header[6] == 0x45 && // E + header[7] == 0x78 && // X + header[8] == 0x69 && // I + header[9] == 0x66 && // F + header[10] == 0x00; + + return isExif; + } + + /// + /// Returns a value indicating whether the given bytes identify Jpeg data. + /// This is a last chance resort for jpegs that contain ICC information. + /// + /// The bytes representing the file header. + /// The + private static bool IsJpeg(byte[] header) + { + bool isJpg = + header[0] == 0xFF && // 255 + header[1] == 0xD8; // 216 + + return isJpg; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id b/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id new file mode 100644 index 000000000..fce8df5dc --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id @@ -0,0 +1 @@ +3ef7ce74c01efdb8145d6b3d03c937c862025a00 \ No newline at end of file diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs b/src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs new file mode 100644 index 000000000..9a267f79b --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/JpegEncoder.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + /// + /// Encoder for writing the data image to a stream in jpeg format. + /// + public class JpegEncoder : IImageEncoder + { + /// + /// The quality used to encode the image. + /// + private int quality = 75; + + /// + /// The subsamples scheme used to encode the image. + /// + private JpegSubsample subsample = JpegSubsample.Ratio420; + + /// + /// Whether subsampling has been specifically set. + /// + private bool subsampleSet; + + /// + /// Gets or sets the quality, that will be used to encode the image. Quality + /// index must be between 0 and 100 (compression from max to min). + /// + /// + /// If the quality is less than or equal to 80, the subsampling ratio will switch to + /// + /// The quality of the jpg image from 0 to 100. + public int Quality + { + get { return this.quality; } + set { this.quality = value.Clamp(1, 100); } + } + + /// + /// Gets or sets the subsample ration, that will be used to encode the image. + /// + /// The subsample ratio of the jpg image. + public JpegSubsample Subsample + { + get { return this.subsample; } + set + { + this.subsample = value; + this.subsampleSet = true; + } + } + + /// + public string MimeType => "image/jpeg"; + + /// + public string Extension => "jpg"; + + /// + public bool IsSupportedFileExtension(string extension) + { + Guard.NotNullOrEmpty(extension, "extension"); + + if (extension.StartsWith(".")) + { + extension = extension.Substring(1); + } + + return extension.Equals(this.Extension, StringComparison.OrdinalIgnoreCase) || + extension.Equals("jpeg", StringComparison.OrdinalIgnoreCase) || + extension.Equals("jfif", StringComparison.OrdinalIgnoreCase); + } + + /// + public void Encode(ImageBase image, Stream stream) + where T : IPackedVector + where TP : struct + { + JpegEncoderCore encode = new JpegEncoderCore(); + if (this.subsampleSet) + { + encode.Encode(image, stream, this.Quality, this.Subsample); + } + else + { + encode.Encode(image, stream, this.Quality, this.Quality >= 80 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420); + } + } + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs b/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs new file mode 100644 index 000000000..c9a8427d5 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs @@ -0,0 +1,796 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + internal class JpegEncoderCore + { + /// + /// Maps from the zig-zag ordering to the natural ordering. For example, + /// unzig[3] is the column and row of the fourth element in zig-zag order. The + /// value is 16, which means first column (16%8 == 0) and third row (16/8 == 2). + /// + private static readonly int[] Unzig = + { + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, + 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, + 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, + 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, + }; + + private const int NQuantIndex = 2; + + /// + /// Counts the number of bits needed to hold an integer. + /// + private readonly byte[] bitCount = + { + 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, + }; + + /// + /// The unscaled quantization tables in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from section K.1 after converting from natural to + /// zig-zag order. + /// + private readonly byte[,] unscaledQuant = { + { + // Luminance. + 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, + 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, 57, 51, + 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80, 109, 81, + 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, 100, 120, 92, + 101, 103, 99, + }, + { + // Chrominance. + 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + } + }; + + /// + /// The Huffman encoding specifications. + /// This encoder uses the same Huffman encoding for all images. + /// + private readonly HuffmanSpec[] theHuffmanSpec = { + // Luminance DC. + new HuffmanSpec( + new byte[] + { + 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 + }, + new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }), + new HuffmanSpec( + new byte[] + { + 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125 + }, + new byte[] + { + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, + 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, + 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, + 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, + 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, + 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, + 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, + 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa + }), + new HuffmanSpec( + new byte[] + { + 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 + }, + new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }), + + // Chrominance AC. + new HuffmanSpec( + new byte[] + { + 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119 + }, + new byte[] + { + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, + 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, + 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, + 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, + 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, + 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, + 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, + 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, + 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, + 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, + 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, + }) + }; + + /// + /// A compiled look-up table representation of a huffmanSpec. + /// Each value maps to a uint32 of which the 8 most significant bits hold the + /// codeword size in bits and the 24 least significant bits hold the codeword. + /// The maximum codeword size is 16 bits. + /// + private class HuffmanLut + { + public readonly uint[] Values; + + public HuffmanLut(HuffmanSpec s) + { + int maxValue = 0; + + foreach (var v in s.Values) + { + if (v > maxValue) maxValue = v; + } + + this.Values = new uint[maxValue + 1]; + + int code = 0; + int k = 0; + + for (int i = 0; i < s.Count.Length; i++) + { + int nBits = (i + 1) << 24; + for (int j = 0; j < s.Count[i]; j++) + { + this.Values[s.Values[k]] = (uint)(nBits | code); + code++; + k++; + } + + code <<= 1; + } + } + } + + // w is the writer to write to. err is the first error encountered during + // writing. All attempted writes after the first error become no-ops. + private Stream outputStream; + + /// + /// A scratch buffer to reduce allocations. + /// + private readonly byte[] buffer = new byte[16]; + + /// + /// The accumulated bits to write to the stream. + /// + private uint bits; + + /// + /// The accumulated bits to write to the stream. + /// + private uint nBits; + + /// + /// The scaled quantization tables, in zig-zag order. + /// + private readonly byte[][] quant = new byte[NQuantIndex][]; // [Block.blockSize]; + + // The compiled representations of theHuffmanSpec. + private readonly HuffmanLut[] theHuffmanLUT = new HuffmanLut[4]; + + /// + /// The subsampling method to use. + /// + private JpegSubsample subsample; + + /// + /// Writes the given byte to the stream. + /// + /// + private void WriteByte(byte b) + { + var data = new byte[1]; + data[0] = b; + this.outputStream.Write(data, 0, 1); + } + + /// + /// Emits the least significant nBits bits of bits to the bit-stream. + /// The precondition is bits < 1<<nBits && nBits <= 16. + /// + /// + /// + private void Emit(uint bits, uint nBits) + { + nBits += this.nBits; + bits <<= (int)(32 - nBits); + bits |= this.bits; + while (nBits >= 8) + { + byte b = (byte)(bits >> 24); + this.WriteByte(b); + if (b == 0xff) this.WriteByte(0x00); + bits <<= 8; + nBits -= 8; + } + + this.bits = bits; + this.nBits = nBits; + } + + /// + /// Emits the given value with the given Huffman encoder. + /// + /// The index of the Huffman encoder + /// The value to encode. + private void EmitHuff(HuffIndex index, int value) + { + uint x = this.theHuffmanLUT[(int)index].Values[value]; + this.Emit(x & ((1 << 24) - 1), x >> 24); + } + + /// + /// Emits a run of runLength copies of value encoded with the given Huffman encoder. + /// + /// The index of the Huffman encoder + /// The number of copies to encode. + /// The value to encode. + private void EmitHuffRLE(HuffIndex index, int runLength, int value) + { + int a = value; + int b = value; + if (a < 0) + { + a = -value; + b = value - 1; + } + + uint bt; + if (a < 0x100) + { + bt = this.bitCount[a]; + } + else + { + bt = 8 + (uint)this.bitCount[a >> 8]; + } + + this.EmitHuff(index, (int)((uint)(runLength << 4) | bt)); + if (bt > 0) + { + this.Emit((uint)b & (uint)((1 << ((int)bt)) - 1), bt); + } + } + + + /// + /// Writes a block of pixel data using the given quantization table, + /// returning the post-quantized DC value of the DCT-transformed block. + /// The block is in natural (not zig-zag) order. + /// + /// The block to write. + /// The quantization table index. + /// The previous DC value. + /// + private int WriteBlock(Block block, QuantIndex index, int prevDC) + { + FDCT.Transform(block); + + // Emit the DC delta. + int dc = Round(block[0], 8 * this.quant[(int)index][0]); + this.EmitHuffRLE((HuffIndex)(2 * (int)index + 0), 0, dc - prevDC); + + // Emit the AC components. + var h = (HuffIndex)(2 * (int)index + 1); + int runLength = 0; + + for (int zig = 1; zig < Block.BlockSize; zig++) + { + int ac = Round(block[Unzig[zig]], 8 * this.quant[(int)index][zig]); + + if (ac == 0) + { + runLength++; + } + else + { + while (runLength > 15) + { + this.EmitHuff(h, 0xf0); + runLength -= 16; + } + + this.EmitHuffRLE(h, runLength, ac); + runLength = 0; + } + } + + if (runLength > 0) this.EmitHuff(h, 0x00); + return dc; + } + + // toYCbCr converts the 8x8 region of m whose top-left corner is p to its + // YCbCr values. + private void ToYCbCr(IPixelAccessor pixels, int x, int y, Block yBlock, Block cbBlock, Block crBlock) + where T : IPackedVector + where TP : struct + { + int xmax = pixels.Width - 1; + int ymax = pixels.Height - 1; + for (int j = 0; j < 8; j++) + { + for (int i = 0; i < 8; i++) + { + // Bytes are expected in r->g->b->a oder. + byte[] pixel = pixels[Math.Min(x + i, xmax), Math.Min(y + j, ymax)].ToBytes(); + + YCbCr color = new Color(pixel[0], pixel[1], pixel[2], pixel[3]); + int index = (8 * j) + i; + yBlock[index] = (int)color.Y; + cbBlock[index] = (int)color.Cb; + crBlock[index] = (int)color.Cr; + } + } + } + + /// + /// Scales the 16x16 region represented by the 4 src blocks to the 8x8 + /// dst block. + /// + /// The destination block array + /// The source block array. + private void Scale16X16_8X8(Block destination, Block[] source) + { + for (int i = 0; i < 4; i++) + { + int dstOff = ((i & 2) << 4) | ((i & 1) << 2); + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + int j = 16 * y + 2 * x; + int sum = source[i][j] + source[i][j + 1] + source[i][j + 8] + source[i][j + 9]; + destination[8 * y + x + dstOff] = (sum + 2) / 4; + } + } + } + } + + // The SOS marker "\xff\xda" followed by 8 bytes: + // - the marker length "\x00\x08", + // - the number of components "\x01", + // - component 1 uses DC table 0 and AC table 0 "\x01\x00", + // - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for + // sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al) + // should be 0x00, 0x3f, 0x00<<4 | 0x00. + private readonly byte[] SOSHeaderY = + { + JpegConstants.Markers.XFF, JpegConstants.Markers.SOS, + 0x00, 0x08, // Length (high byte, low byte), must be 6 + 2 * (number of components in scan) + 0x01, // Number of components in a scan, 1 + 0x01, // Component Id Y + 0x00, // DC/AC Huffman table + 0x00, // Ss - Start of spectral selection. + 0x3f, // Se - End of spectral selection. + 0x00 // Ah + Ah (Successive approximation bit position high + low) + }; + + // The SOS marker "\xff\xda" followed by 12 bytes: + // - the marker length "\x00\x0c", + // - the number of components "\x03", + // - component 1 uses DC table 0 and AC table 0 "\x01\x00", + // - component 2 uses DC table 1 and AC table 1 "\x02\x11", + // - component 3 uses DC table 1 and AC table 1 "\x03\x11", + // - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for + // sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al) + // should be 0x00, 0x3f, 0x00<<4 | 0x00. + private readonly byte[] SOSHeaderYCbCr = + { + JpegConstants.Markers.XFF, JpegConstants.Markers.SOS, + 0x00, 0x0c, // Length (high byte, low byte), must be 6 + 2 * (number of components in scan) + 0x03, // Number of components in a scan, 3 + 0x01, // Component Id Y + 0x00, // DC/AC Huffman table + 0x02, // Component Id Cb + 0x11, // DC/AC Huffman table + 0x03, // Component Id Cr + 0x11, // DC/AC Huffman table + 0x00, // Ss - Start of spectral selection. + 0x3f, // Se - End of spectral selection. + 0x00 // Ah + Ah (Successive approximation bit position high + low) + }; + + // Encode writes the Image m to w in JPEG 4:2:0 baseline format with the given + // options. Default parameters are used if a nil *Options is passed. + public void Encode(ImageBase image, Stream stream, int quality, JpegSubsample sample) + where T : IPackedVector + where TP : struct + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + ushort max = JpegConstants.MaxLength; + if (image.Width >= max || image.Height >= max) + { + throw new ImageFormatException($"Image is too large to encode at {image.Width}x{image.Height}."); + } + + this.outputStream = stream; + this.subsample = sample; + + // TODO: This should be static should it not? + for (int i = 0; i < this.theHuffmanSpec.Length; i++) + { + this.theHuffmanLUT[i] = new HuffmanLut(this.theHuffmanSpec[i]); + } + + for (int i = 0; i < NQuantIndex; i++) + { + this.quant[i] = new byte[Block.BlockSize]; + } + + if (quality < 1) quality = 1; + if (quality > 100) quality = 100; + + // Convert from a quality rating to a scaling factor. + int scale; + if (quality < 50) + { + scale = 5000 / quality; + } + else + { + scale = 200 - quality * 2; + } + + // Initialize the quantization tables. + for (int i = 0; i < NQuantIndex; i++) + { + for (int j = 0; j < Block.BlockSize; j++) + { + int x = this.unscaledQuant[i, j]; + x = (x * scale + 50) / 100; + if (x < 1) x = 1; + if (x > 255) x = 255; + this.quant[i][j] = (byte)x; + } + } + + // Compute number of components based on input image type. + int componentCount = 3; + + // Write the Start Of Image marker. + // TODO: JFIF header etc. + this.buffer[0] = 0xff; + this.buffer[1] = 0xd8; + stream.Write(this.buffer, 0, 2); + + // Write the quantization tables. + this.WriteDQT(); + + // Write the image dimensions. + this.WriteSOF0(image.Width, image.Height, componentCount); + + // Write the Huffman tables. + this.WriteDHT(componentCount); + + // Write the image data. + using (IPixelAccessor pixels = image.Lock()) + { + this.WriteSOS(pixels); + } + + // Write the End Of Image marker. + this.buffer[0] = 0xff; + this.buffer[1] = 0xd9; + stream.Write(this.buffer, 0, 2); + stream.Flush(); + } + + /// + /// Gets the quotient of the two numbers rounded to the nearest integer, instead of rounded to zero. + /// + /// The value to divide. + /// The value to divide by. + /// The + private static int Round(int dividend, int divisor) + { + if (dividend >= 0) + { + return (dividend + (divisor >> 1)) / divisor; + } + + return -((-dividend + (divisor >> 1)) / divisor); + } + + /// + /// Writes the Define Quantization Marker and tables. + /// + private void WriteDQT() + { + int markerlen = 2 + NQuantIndex * (1 + Block.BlockSize); + this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen); + for (int i = 0; i < NQuantIndex; i++) + { + this.WriteByte((byte)i); + this.outputStream.Write(this.quant[i], 0, this.quant[i].Length); + } + } + + /// + /// Writes the Start Of Frame (Baseline) marker + /// + /// The width of the image + /// The height of the image + /// + private void WriteSOF0(int width, int height, int componentCount) + { + // "default" to 4:2:0 + byte[] subsamples = { 0x22, 0x11, 0x11 }; + byte[] chroma = { 0x00, 0x01, 0x01 }; + + switch (this.subsample) + { + case JpegSubsample.Ratio444: + subsamples = new byte[] { 0x11, 0x11, 0x11 }; + break; + case JpegSubsample.Ratio420: + subsamples = new byte[] { 0x22, 0x11, 0x11 }; + break; + } + + // Length (high byte, low byte), 8 + components * 3. + int markerlen = 8 + 3 * componentCount; + this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen); + this.buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported + this.buffer[1] = (byte)(height >> 8); + this.buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported + this.buffer[3] = (byte)(width >> 8); + this.buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported + this.buffer[5] = (byte)componentCount; // Number of components (1 byte), usually 1 = grey scaled, 3 = color YCbCr or YIQ, 4 = color CMYK) + if (componentCount == 1) + { + this.buffer[6] = 1; + + // No subsampling for grayscale images. + this.buffer[7] = 0x11; + this.buffer[8] = 0x00; + } + else + { + for (int i = 0; i < componentCount; i++) + { + this.buffer[3 * i + 6] = (byte)(i + 1); + + // We use 4:2:0 chroma subsampling by default. + this.buffer[3 * i + 7] = subsamples[i]; + this.buffer[3 * i + 8] = chroma[i]; + } + } + + this.outputStream.Write(this.buffer, 0, 3 * (componentCount - 1) + 9); + } + + /// + /// Writes the Define Huffman Table marker and tables. + /// + /// The number of components to write. + private void WriteDHT(int nComponent) + { + byte[] headers = { 0x00, 0x10, 0x01, 0x11 }; + int markerlen = 2; + HuffmanSpec[] specs = this.theHuffmanSpec; + + if (nComponent == 1) + { + // Drop the Chrominance tables. + specs = new[] { this.theHuffmanSpec[0], this.theHuffmanSpec[1] }; + } + + foreach (var s in specs) + { + markerlen += 1 + 16 + s.Values.Length; + } + + this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen); + for (int i = 0; i < specs.Length; i++) + { + HuffmanSpec spec = specs[i]; + + this.WriteByte(headers[i]); + this.outputStream.Write(spec.Count, 0, spec.Count.Length); + this.outputStream.Write(spec.Values, 0, spec.Values.Length); + } + } + + /// + /// Writes the StartOfScan marker. + /// + /// The pixel accessor providing acces to the image pixels. + private void WriteSOS(IPixelAccessor pixels) + where T : IPackedVector + where TP : struct + { + // TODO: We should allow grayscale writing. + this.outputStream.Write(this.SOSHeaderYCbCr, 0, this.SOSHeaderYCbCr.Length); + + switch (this.subsample) + { + case JpegSubsample.Ratio444: + this.Encode444(pixels); + break; + case JpegSubsample.Ratio420: + this.Encode420(pixels); + break; + } + + // Pad the last byte with 1's. + this.Emit(0x7f, 7); + } + + + + /// + /// Encodes the image with no subsampling. + /// + /// The pixel accessor providing acces to the image pixels. + private void Encode444(IPixelAccessor pixels) + where T : IPackedVector + where TP : struct + { + Block b = new Block(); + Block cb = new Block(); + Block cr = new Block(); + int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + + for (int y = 0; y < pixels.Height; y += 8) + { + for (int x = 0; x < pixels.Width; x += 8) + { + this.ToYCbCr(pixels, x, y, b, cb, cr); + prevDCY = this.WriteBlock(b, QuantIndex.Luminance, prevDCY); + prevDCCb = this.WriteBlock(cb, QuantIndex.Chrominance, prevDCCb); + prevDCCr = this.WriteBlock(cr, QuantIndex.Chrominance, prevDCCr); + } + } + } + + /// + /// Encodes the image with subsampling. The Cb and Cr components are each subsampled + /// at a factor of 2 both horizontally and vertically. + /// + /// The pixel accessor providing acces to the image pixels. + private void Encode420(IPixelAccessor pixels) + where T : IPackedVector + where TP : struct + { + Block b = new Block(); + Block[] cb = new Block[4]; + Block[] cr = new Block[4]; + int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + + for (int i = 0; i < 4; i++) cb[i] = new Block(); + for (int i = 0; i < 4; i++) cr[i] = new Block(); + + for (int y = 0; y < pixels.Height; y += 16) + { + for (int x = 0; x < pixels.Width; x += 16) + { + for (int i = 0; i < 4; i++) + { + int xOff = (i & 1) * 8; + int yOff = (i & 2) * 4; + + this.ToYCbCr(pixels, x + xOff, y + yOff, b, cb[i], cr[i]); + prevDCY = this.WriteBlock(b, QuantIndex.Luminance, prevDCY); + } + + this.Scale16X16_8X8(b, cb); + prevDCCb = this.WriteBlock(b, QuantIndex.Chrominance, prevDCCb); + this.Scale16X16_8X8(b, cr); + prevDCCr = this.WriteBlock(b, QuantIndex.Chrominance, prevDCCr); + } + } + } + + /// + /// Writes the header for a marker with the given length. + /// + /// The marker to write. + /// The marker length. + private void WriteMarkerHeader(byte marker, int length) + { + // Markers are always prefixed with with 0xff. + this.buffer[0] = JpegConstants.Markers.XFF; + this.buffer[1] = marker; + this.buffer[2] = (byte)(length >> 8); + this.buffer[3] = (byte)(length & 0xff); + this.outputStream.Write(this.buffer, 0, 4); + } + + /// + /// Enumerates the Huffman tables + /// + private enum HuffIndex + { + LuminanceDC = 0, + + LuminanceAC = 1, + + ChrominanceDC = 2, + + ChrominanceAC = 3, + } + + /// + /// Enumerates the quantization tables + /// + private enum QuantIndex + { + /// + /// Luminance + /// + Luminance = 0, + + /// + /// Chrominance + /// + Chrominance = 1, + } + + /// + /// The Huffman encoding specifications. + /// + private struct HuffmanSpec + { + /// + /// Initializes a n ew instance of the struct. + /// + /// The number of codes. + /// The decoded values. + public HuffmanSpec(byte[] count, byte[] values) + { + this.Count = count; + this.Values = values; + } + + /// + /// Gets count[i] - The number of codes of length i bits. + /// + public readonly byte[] Count; + + /// + /// Gets value[i] - The decoded value of the i'th codeword. + /// + public readonly byte[] Values; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegFormat.cs b/src/ImageProcessorCore/Formats/Jpg/JpegFormat.cs new file mode 100644 index 000000000..ec9ceb6dc --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/JpegFormat.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Encapsulates the means to encode and decode jpeg images. + /// + public class JpegFormat : IImageFormat + { + /// + public IImageDecoder Decoder => new JpegDecoder(); + + /// + public IImageEncoder Encoder => new JpegEncoder(); + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegSubsample.cs b/src/ImageProcessorCore/Formats/Jpg/JpegSubsample.cs new file mode 100644 index 000000000..6098f6377 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/JpegSubsample.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Enumerates the chroma subsampling method applied to the image. + /// + public enum JpegSubsample + { + /// + /// High Quality - Each of the three Y'CbCr components have the same sample rate, + /// thus there is no chroma subsampling. + /// + Ratio444, + + /// + /// Medium Quality - The horizontal sampling is halved and the Cb and Cr channels are only + /// sampled on each alternate line. + /// + Ratio420 + } +} diff --git a/src/ImageProcessorCore/Formats/Jpg/README.md b/src/ImageProcessorCore/Formats/Jpg/README.md new file mode 100644 index 000000000..54bc14847 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Jpg/README.md @@ -0,0 +1,3 @@ +Encoder/Decoder adapted and extended from: + +https://golang.org/src/image/jpeg/ \ No newline at end of file diff --git a/src/ImageProcessorCore/Image/ImageExtensions.cs b/src/ImageProcessorCore/Image/ImageExtensions.cs index 53133207f..28ba60f08 100644 --- a/src/ImageProcessorCore/Image/ImageExtensions.cs +++ b/src/ImageProcessorCore/Image/ImageExtensions.cs @@ -46,14 +46,19 @@ namespace ImageProcessorCore where TP : struct => new PngEncoder { Quality = quality }.Encode(source, stream); - ///// - ///// Saves the image to the given stream with the jpeg format. - ///// - ///// The image this method extends. - ///// The stream to save the image to. - ///// The quality to save the image to. Between 1 and 100. - ///// Thrown if the stream is null. - //public static void SaveAsJpeg(this ImageBase source, Stream stream, int quality = 75) => new JpegEncoder { Quality = quality }.Encode(source, stream); + /// + /// Saves the image to the given stream with the jpeg format. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The stream to save the image to. + /// The quality to save the image to. Between 1 and 100. + /// Thrown if the stream is null. + public static void SaveAsJpeg(this ImageBase source, Stream stream, int quality = 75) + where T : IPackedVector + where TP : struct + => new JpegEncoder { Quality = quality }.Encode(source, stream); /// /// Saves the image to the given stream with the gif format.