Add @EliotJones's BigGustave jpgs classes

This commit is contained in:
BobLd 2023-07-27 18:23:05 +01:00
parent 8a82500427
commit 76c6e9436d
16 changed files with 1647 additions and 0 deletions

View File

@ -0,0 +1,54 @@
namespace BigGustave.Jpgs
{
using System;
using System.Collections.Generic;
internal class BitStream
{
private int bitOffset;
private readonly IReadOnlyList<byte> data;
public BitStream(IReadOnlyList<byte> data)
{
this.data = data;
}
public int Read()
{
var byteIndex = bitOffset / 8;
if (byteIndex >= data.Count)
{
return -1;
}
var byteVal = data[byteIndex];
var withinByteIndex = bitOffset - (byteIndex * 8);
bitOffset++;
// TODO: LSB?
return ((1 << (7 - withinByteIndex)) & byteVal) > 0 ? 1 : 0;
}
public int ReadNBits(int length)
{
var result = 0;
for (var i = 0; i < length; i++)
{
var bit = Read();
if (bit < 0)
{
return 0;
throw new InvalidOperationException($"Encountered end of bit stream while trying to read {length} bytes.");
}
result = (result << 1) + bit;
}
return result;
}
}
}

View File

@ -0,0 +1,45 @@
namespace BigGustave.Jpgs
{
using System;
using System.IO;
using System.Text;
/// <summary>
/// A comment in a JPG file.
/// </summary>
public class Comment
{
/// <summary>
/// The bytes of the comment.
/// </summary>
public byte[] Bytes { get; }
/// <summary>
/// The comment bytes interpreted in ASCII.
/// </summary>
public string Text { get; }
private Comment(byte[] bytes)
{
Bytes = bytes;
Text = Encoding.ASCII.GetString(bytes);
}
internal static Comment ReadFromMarker(Stream stream)
{
var offset = stream.Position;
var length = stream.ReadShort();
// Read comment text.
var bytes = new byte[length];
var read = stream.Read(bytes, 0, bytes.Length);
if (read != bytes.Length)
{
throw new InvalidOperationException($"Failed to read comment of length {length} at offset {offset}. Read {read} bytes instead.");
}
return new Comment(bytes);
}
}
}

View File

@ -0,0 +1,177 @@
namespace BigGustave.Jpgs
{
using System;
using System.Collections.Generic;
using System.IO;
internal class Frame
{
public FrameType FrameType { get; }
/// <summary>
/// Offset from the start of the file to this table's marker.
/// </summary>
public long Offset { get; }
public byte BitsPerSample { get; }
public short ImageHeight { get; }
public short ImageWidth { get; }
/// <summary>
/// Generally 1 = grayscale, 3 = YCbCr or YIQ, 4 = CMYK.
/// </summary>
public byte NumberOfComponents { get; }
public FrameComponentSpecificationParameters[] Components { get; }
public int McusPerX { get; }
public int McusPerY { get; }
public int MaxHorizontalSamplingFactor { get; }
public int MaxVerticalSamplingFactor { get; }
public List<Scan> Scans { get; } = new List<Scan>();
public Frame(
FrameType frameType,
long offset,
byte bitsPerSample,
short imageHeight,
short imageWidth,
byte numberOfComponents,
FrameComponentSpecificationParameters[] components,
int mcusPerX,
int mcusPerY,
int maxHorizontalSamplingFactor,
int maxVerticalSamplingFactor)
{
FrameType = frameType;
Offset = offset;
BitsPerSample = bitsPerSample;
ImageHeight = imageHeight;
ImageWidth = imageWidth;
NumberOfComponents = numberOfComponents;
Components = components;
McusPerX = mcusPerX;
McusPerY = mcusPerY;
MaxHorizontalSamplingFactor = maxHorizontalSamplingFactor;
MaxVerticalSamplingFactor = maxVerticalSamplingFactor;
}
public static Frame ReadFromMarker(Stream stream, bool strictMode, byte markerByte)
{
var frameType = (FrameType)markerByte;
var offset = stream.Position;
var length = stream.ReadShort();
var bitsPerSample = stream.ReadByteActual();
if (strictMode && bitsPerSample != 8)
{
throw new InvalidOperationException($"Sample precision should be 8 for baseline DCT frames. Got: {bitsPerSample} at offset {stream.Position}.");
}
var imageHeight = stream.ReadShort();
var imageWidth = stream.ReadShort();
var numberOfComponents = stream.ReadByteActual();
var frameComponents = new FrameComponentSpecificationParameters[numberOfComponents];
var maxHorizontalFactor = 0;
var maxVerticalFactor = 0;
for (var i = 0; i < frameComponents.Length; i++)
{
var identifier = stream.ReadByteActual();
var (horizontal, vertical) = stream.ReadNibblePair();
var quantizationTableSelector = stream.ReadByteActual();
if (horizontal > maxHorizontalFactor)
{
maxHorizontalFactor = horizontal;
}
if (vertical > maxVerticalFactor)
{
maxVerticalFactor = vertical;
}
frameComponents[i] = new FrameComponentSpecificationParameters(identifier, horizontal, vertical, quantizationTableSelector);
}
if (strictMode && stream.Position - offset != length)
{
throw new InvalidOperationException($"Read incorrect number of bytes for frame header ({stream.Position - offset})," +
$" should have read {length} bytes at offset {offset}..");
}
// When a frame contains more than 1 component (?).
var adjustX = imageWidth % 8 == 0 ? 0 : 1;
var adjustY = imageHeight % 8 == 0 ? 0 : 1;
maxVerticalFactor = maxVerticalFactor > 0 ? maxVerticalFactor : 1;
maxHorizontalFactor = maxHorizontalFactor > 0 ? maxHorizontalFactor : 1;
var mcusPerX = (imageWidth / 8 + adjustX) / (double)maxHorizontalFactor;
var mcusPerY = (imageHeight / 8 + adjustY) / (double)maxVerticalFactor;
return new Frame(
frameType,
offset,
bitsPerSample,
imageHeight,
imageWidth,
numberOfComponents,
frameComponents,
(byte)Math.Ceiling(mcusPerX),
(byte)Math.Ceiling(mcusPerY),
maxHorizontalFactor,
maxVerticalFactor);
}
public readonly struct FrameComponentSpecificationParameters
{
/// <summary>
/// Assigns a unique label to the ith component in the sequence of frame component specification parameters.
/// These values shall be used in the scan headers to identify the components in the scan.
/// The value of Ci shall be different from the values of C1 through Ci 1.
/// </summary>
public byte ComponentIdentifier { get; }
/// <summary>
/// Specifies the relationship between the component horizontal dimension and maximum image dimension X;
/// also specifies the number of horizontal data units of component Ci in each MCU, when more than one component is encoded in a scan.
/// </summary>
public byte HorizontalSamplingFactor { get; }
/// <summary>
/// Specifies the relationship between the component vertical dimension and maximum image dimension Y ;
/// also specifies the number of vertical data units of component Ci in each MCU, when more than one component is encoded in a scan.
/// </summary>
public byte VerticalSamplingFactor { get; }
/// <summary>
/// Specifies one of four possible quantization table destinations
/// from which the quantization table to use for de-quantization of DCT coefficients of component Ci is retrieved. If
/// the decoding process uses the de-quantization procedure, this table shall have been installed in this destination
/// by the time the decoder is ready to decode the scan(s) containing component Ci.
/// The destination shall not be re-specified, or its contents changed, until all scans containing Ci have been completed.
/// </summary>
public byte DestinationQuantizationTableSelector { get; }
public FrameComponentSpecificationParameters(byte componentIdentifier, byte horizontalSamplingFactor,
byte verticalSamplingFactor,
byte destinationQuantizationTableSelector)
{
ComponentIdentifier = componentIdentifier;
HorizontalSamplingFactor = horizontalSamplingFactor;
VerticalSamplingFactor = verticalSamplingFactor;
DestinationQuantizationTableSelector = destinationQuantizationTableSelector;
}
}
}
}

View File

@ -0,0 +1,19 @@
namespace BigGustave.Jpgs
{
internal enum FrameType : byte
{
BaselineHuffman = 0xC0,
ExtendedSequentialHuffman = 0xC1,
ProgressiveHuffman = 0xC2,
LosslessHuffman = 0xC3,
DifferentialSequentialDctHuffman = 0xC5,
DifferentialProgressiveDctHuffman = 0xC6,
DifferentialLosslessHuffman = 0xC7,
ExtendedSequentialArithmetic = 0xC9,
ProgressiveArithmetic = 0xCA,
LosslessArithmetic = 0xCB,
DifferentialSequentialDctArithmetic = 0xCD,
DifferentialProgressiveDctArithmetic = 0xCE,
DifferentialLosslessArithmetic = 0xCF,
}
}

View File

@ -0,0 +1,17 @@
namespace BigGustave.Jpgs
{
internal static class FrameTypeExtensions
{
public static bool IsHuffman(FrameType frameType)
{
var b = (byte) frameType;
return b >= 0xC0 && b <= 0xC7 && b != 0xC4;
}
public static bool IsArithmetic(FrameType frameType)
{
var b = (byte)frameType;
return b >= 0xC9 && b <= 0xCF && b != 0xCC;
}
}
}

View File

@ -0,0 +1,179 @@
namespace BigGustave.Jpgs
{
using System;
internal class HuffmanTable
{
public Node Root { get; }
public HuffmanTable(Node root)
{
Root = root;
}
public static HuffmanTable FromSpecification(HuffmanTableSpecification specification)
{
var root = new Node(null);
var elementIndex = 0;
for (var lengthIndex = 0; lengthIndex < specification.Lengths.Length; lengthIndex++)
{
var length = specification.Lengths[lengthIndex];
var depth = lengthIndex + 1;
for (var i = 0; i < length; i++)
{
var element = specification.Elements[elementIndex++];
var node = root.GetNextAt(depth);
node.SetValue(element);
}
}
return new HuffmanTable(root);
}
public byte? Read(BitStream stream)
{
var infiniteLoopDetector = 0;
while (true)
{
if (infiniteLoopDetector > 100_000_000)
{
throw new InvalidOperationException();
}
var item = Root;
while (item != null)
{
if (item.Value.HasValue)
{
return item.Value;
}
var direction = stream.Read();
item = direction == 1 ? item.Right : item.Left;
}
infiniteLoopDetector++;
}
}
public class Node
{
private bool isFull;
private readonly int level;
private readonly Node parent;
public bool IsRoot => parent == null;
public bool IsLeaf => Value.HasValue;
public Node Left { get; private set; }
public Node Right { get; private set; }
public byte? Value { get; private set; }
public Node(Node parent)
{
this.parent = parent;
level = parent == null ? 0 : this.parent.level + 1;
}
public Node AddLeft()
{
Left = new Node(this);
return Left;
}
public Node AddRight()
{
Right = new Node(this);
return Right;
}
public void SetValue(byte value)
{
if (Value.HasValue)
{
throw new InvalidOperationException("Cannot overwrite a leaf value.");
}
if (Left != null || Right != null)
{
throw new InvalidOperationException("Cannot set value on non-leaf node.");
}
isFull = true;
Value = value;
parent.ChildOccupied();
}
private void ChildOccupied()
{
if (Left?.isFull == true && Right?.isFull == true)
{
isFull = true;
parent?.ChildOccupied();
}
}
public Node GetNextAt(int depth)
{
if (depth < level || IsLeaf || isFull)
{
return null;
}
if (depth == level)
{
// not a leaf node.
if (Left != null || Right != null)
{
return null;
}
return this;
}
if (Left == null)
{
AddLeft();
}
var result = Left.GetNextAt(depth);
if (result != null)
{
return result;
}
if (Right == null)
{
AddRight();
}
result = Right.GetNextAt(depth);
return result;
}
public override string ToString()
{
if (Value.HasValue)
{
return $"{Value}";
}
return $"[{Left}, {Right}]";
}
}
}
}

View File

@ -0,0 +1,70 @@
namespace BigGustave.Jpgs
{
using System.Collections.Generic;
using System.IO;
internal class HuffmanTableSpecification
{
public byte DestinationIdentifier { get; }
public HuffmanClass TableClass { get; }
public byte[] Lengths { get; }
public byte[] Elements { get; }
public HuffmanTableSpecification(byte destinationIdentifier, HuffmanClass tableClass, byte[] lengths, byte[] elements)
{
DestinationIdentifier = destinationIdentifier;
TableClass = tableClass;
Lengths = lengths;
Elements = elements;
}
public static IReadOnlyList<HuffmanTableSpecification> ReadFromMarker(Stream stream)
{
var tableDefinitionLength = stream.ReadShort();
var results = new List<HuffmanTableSpecification>();
var remainingLength = tableDefinitionLength;
while (remainingLength > 2)
{
var (tableClass, destinationIdentifier) = stream.ReadNibblePair();
var lengths = new byte[16];
var numberOfCodes = 0;
for (var i = 0; i < 16; i++)
{
var val = stream.ReadByteActual();
lengths[i] = val;
numberOfCodes += val;
}
remainingLength -= 17;
var huffmanCodeValues = new byte[numberOfCodes];
for (var i = 0; i < numberOfCodes; i++)
{
huffmanCodeValues[i] = stream.ReadByteActual();
remainingLength--;
}
results.Add(new HuffmanTableSpecification(
destinationIdentifier,
(HuffmanClass)tableClass,
lengths,
huffmanCodeValues));
}
return results;
}
public enum HuffmanClass : byte
{
DcTable = 0,
AcTable = 1
}
}
}

View File

@ -0,0 +1,116 @@
namespace BigGustave.Jpgs
{
using System.IO;
/// <summary>
/// The JFIF format information in the APP0 section.
/// </summary>
public class Jfif
{
/// <summary>
/// The major version.
/// </summary>
public byte MajorVersion { get; }
/// <summary>
/// The minor version.
/// </summary>
public byte MinorVersion { get; }
/// <summary>
/// The units for horizontal/vertical pixel densities.
/// </summary>
public PixelDensityUnit PixelDensityUnit { get; }
/// <summary>
/// The horizontal pixel density in <see cref="PixelDensityUnit"/>s.
/// </summary>
public short HorizontalPixelDensity { get; }
/// <summary>
/// The vertical pixel density in <see cref="PixelDensityUnit"/>s.
/// </summary>
public short VerticalPixelDensity { get; }
/// <summary>
/// The raw bytes of the thumbnail if present (R, G, B).
/// </summary>
public byte[] Thumbnail { get; }
internal Jfif(
byte majorVersion,
byte minorVersion,
PixelDensityUnit pixelDensityUnit,
short horizontalPixelDensity,
short verticalPixelDensity,
byte[] thumbnail)
{
MajorVersion = majorVersion;
MinorVersion = minorVersion;
PixelDensityUnit = pixelDensityUnit;
HorizontalPixelDensity = horizontalPixelDensity;
VerticalPixelDensity = verticalPixelDensity;
Thumbnail = thumbnail;
}
internal static Jfif ReadFromApp0(Stream stream)
{
var pos = stream.Position;
stream.ReadShort();
var b1 = stream.ReadByteActual();
if (b1 != 'J')
{
stream.Seek(pos, SeekOrigin.Begin);
return null;
}
var b2 = stream.ReadByteActual();
if (b2 != 'F')
{
stream.Seek(pos, SeekOrigin.Begin);
return null;
}
var b3 = stream.ReadByteActual();
if (b3 != 'I')
{
stream.Seek(pos, SeekOrigin.Begin);
return null;
}
var b4 = stream.ReadByteActual();
if (b4 != 'F')
{
stream.Seek(pos, SeekOrigin.Begin);
return null;
}
var b5 = stream.ReadByteActual();
if (b5 != 0)
{
stream.Seek(pos, SeekOrigin.Begin);
return null;
}
var major = stream.ReadByteActual();
var minor = stream.ReadByteActual();
var pixelDensity = stream.ReadByteActual();
var horizontalPixelDensity = stream.ReadShort();
var verticalPixelDensity = stream.ReadShort();
var horizontalThumbnailPixelCount = stream.ReadByteActual();
var verticalThumbnailPixelCount = stream.ReadByteActual();
var thumbnailLength = 3 * horizontalThumbnailPixelCount * verticalThumbnailPixelCount;
var thumbnailRgb = new byte[thumbnailLength];
stream.Read(thumbnailRgb, 0, thumbnailRgb.Length);
return new Jfif(major, minor, (PixelDensityUnit)pixelDensity,
horizontalPixelDensity,
verticalPixelDensity,
thumbnailRgb);
}
}
}

View File

@ -0,0 +1,87 @@
namespace BigGustave
{
using System;
using System.Collections.Generic;
using System.IO;
using Jpgs;
using UglyToad.PdfPig.Core;
/// <summary>
/// A JPEG image.
/// </summary>
public class Jpg
{
private readonly byte[] rawData;
/// <summary>
/// The width of the image in pixels.
/// </summary>
public int Width { get; }
/// <summary>
/// The height of the image in pixels.
/// </summary>
public int Height { get; }
/// <summary>
/// The JFIF format APP0 section from the image data.
/// </summary>
public Jfif Jfif { get; set; }
/// <summary>
/// Any comments found in the file.
/// </summary>
public IReadOnlyList<Comment> Comments { get; }
/// <summary>
/// Create a new <see cref="Jpg"/>.
/// </summary>
internal Jpg(int width, int height, byte[] rawData, Jfif jfif, IReadOnlyList<Comment> comments)
{
Width = width;
Height = height;
Jfif = jfif;
Comments = comments ?? EmptyArray<Comment>.Instance;
this.rawData = rawData;
}
/*
/// <summary>
/// Get the pixel at the given column and row (x, y).
/// </summary>
/// <remarks>
/// Pixel values are generated on demand from the underlying data to prevent holding many items in memory at once, so consumers
/// should cache values if they're going to be looped over many times.
/// </remarks>
/// <param name="x">The x coordinate (column).</param>
/// <param name="y">The y coordinate (row).</param>
/// <returns>The pixel at the coordinate.</returns>
public Pixel GetPixel(int x, int y)
{
if (x < 0 || x >= Width)
{
throw new ArgumentOutOfRangeException(nameof(x), $"Could not retrieve pixel value at x coordinate: {x}.");
}
if (y < 0 || y >= Height)
{
throw new ArgumentOutOfRangeException(nameof(x), $"Could not retrieve pixel value at y coordinate: {y}.");
}
var flatIndexR = (y * 3) * Width + (x * 3);
var r = rawData[flatIndexR];
var g = rawData[flatIndexR + 1];
var b = rawData[flatIndexR + 2];
return new Pixel(r, g, b);
}
*/
/// <summary>
/// Open and parse a JPG file from the stream.
/// </summary>
public static Jpg Open(Stream stream) => JpgOpener.Open(stream, true);
}
}

View File

@ -0,0 +1,42 @@
namespace BigGustave.Jpgs
{
internal static class JpgDecodeUtil
{
public static readonly byte[] ZigZagPattern = new byte[]
{
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
};
public static int GetDcDifferenceOrAcCoefficient(int category, int value)
{
/*
* This code is a bit confusing, basically the DC coefficient in an MCU is encoded first as a category then as the bits being the value of the difference.
* The relationship between the category and the value is a bit unclear, but the difference magnitude categories for DC coding are explained in more
* detail in the "Value Category and Bitstream Table" section here: https://koushtav.me/jpeg/tutorial/2017/11/25/lets-write-a-simple-jpeg-library-part-1/#encoding-the-dc-coeffs.
*/
if (category == 0)
{
return 0;
}
// The most significant bit is 1 for positive and 0 for negative values.
var isPositive = (value >> (category - 1)) > 0;
if (isPositive)
{
return value;
}
var lower = -(2 << (category - 1)) + 1;
return lower + value;
}
}
}

View File

@ -0,0 +1,101 @@
namespace BigGustave.Jpgs
{
internal enum JpgMarkers : byte
{
/// <summary>
/// Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, and component subsampling.
/// </summary>
StartOfBaselineDctHuffmanFrame = 0xC0,
/// <summary>
/// Indicates that this is a extended sequential DCT-based JPEG.
/// </summary>
StartOfExtendedSequentialDctHuffmanFrame = 0xC1,
/// <summary>
/// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, and component subsampling.
/// </summary>
StartOfProgressiveDctHuffmanFrame = 0xC2,
StartOfLosslessHuffmanFrame = 0xC3,
/// <summary>
/// Specifies one or more Huffman tables.
/// </summary>
DefineHuffmanTable = 0xC4,
StartOfDifferentialSequentialDctHuffmanFrame = 0xC5,
StartOfDifferentialProgressiveDctHuffmanFrame = 0xC6,
StartOfDifferentialLosslessHuffmanFrame = 0xC7,
StartOfExtendedSequentialDctArithmeticFrame = 0xC9,
StartOfProgressiveDctArithmeticFrame = 0xCA,
StartOfLosslessArithmeticFrame = 0xCB,
DefineArithmeticCodingConditioning = 0xCC,
StartOfDifferentialSequentialDctArithmeticFrame = 0xCD,
StartOfDifferentialProgressiveDctArithmeticFrame = 0xCE,
StartOfDifferentialLosslessArithmeticFrame = 0xCF,
/// <summary>
/// Begins a top-to-bottom scan of the image. In baseline images, there is generally a single scan.
/// Progressive images usually contain multiple scans.
/// </summary>
StartOfScan = 0xDA,
/// <summary>
/// Specifies one or more quantization tables.
/// </summary>
DefineQuantizationTable = 0xDB,
/// <summary>
/// Specifies the interval between RSTn markers, in Minimum Coded Units (MCUs).
/// This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment.
/// </summary>
DefineRestartInterval = 0xDD,
/// <summary>
/// Inserted every r macroblocks.
/// </summary>
Restart0 = 0xD0,
/// <summary>
/// Inserted every r macroblocks.
/// </summary>
Restart1 = 0xD1,
/// <summary>
/// Inserted every r macroblocks.
/// </summary>
Restart2 = 0xD2,
/// <summary>
/// Inserted every r macroblocks.
/// </summary>
Restart3 = 0xD3,
/// <summary>
/// Inserted every r macroblocks.
/// </summary>
Restart4 = 0xD4,
/// <summary>
/// Inserted every r macroblocks.
/// </summary>
Restart5 = 0xD5,
/// <summary>
/// Inserted every r macroblocks.
/// </summary>
Restart6 = 0xD6,
/// <summary>
/// Inserted every r macroblocks.
/// </summary>
Restart7 = 0xD7,
/// <summary>
/// Marks the start of a JPEG image file.
/// </summary>
StartOfImage = 0xD8,
/// <summary>
/// Marks the end of a JPEG image file.
/// </summary>
EndOfImage = 0xD9,
ApplicationSpecific0 = 0xE0,
ApplicationSpecific1 = 0xE1,
ApplicationSpecific2 = 0xE2,
ApplicationSpecific3 = 0xE3,
ApplicationSpecific4 = 0xE4,
ApplicationSpecific5 = 0xE5,
ApplicationSpecific6 = 0xE6,
ApplicationSpecific7 = 0xE7,
ApplicationSpecific8 = 0xE8,
ApplicationSpecific9 = 0xE9,
/// <summary>
/// Marks a text comment.
/// </summary>
Comment = 0xFE
}
}

View File

@ -0,0 +1,375 @@
namespace BigGustave.Jpgs
{
using System;
using System.Collections.Generic;
using System.IO;
internal static class JpgOpener
{
private const byte MarkerStart = 255;
private const byte StartOfImage = 216;
public static Jpg Open(Stream stream, bool strictMode)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
if (!stream.CanRead)
{
throw new ArgumentException($"The provided stream of type {stream.GetType().FullName} was not readable.");
}
if (!HasJpgHeader(stream) && strictMode)
{
throw new ArgumentException("The provided stream did not start with the JPEG header.");
}
var jfif = default(Jfif);
var comments = new List<Comment>();
var quantizationTables = new Dictionary<int, QuantizationTableSpecification>();
var dcHuffmanTables = new Dictionary<int, HuffmanTable>();
var acHuffmanTables = new Dictionary<int, HuffmanTable>();
var rgbData = new byte[0];
var frames = new List<Frame>();
var marker = stream.ReadSegmentMarker();
var markerType = (JpgMarkers)marker;
while (markerType != JpgMarkers.EndOfImage)
{
var skipData = true;
switch (markerType)
{
case JpgMarkers.ApplicationSpecific0:
jfif = Jfif.ReadFromApp0(stream);
break;
case JpgMarkers.Comment:
skipData = false;
var comment = Comment.ReadFromMarker(stream);
comments.Add(comment);
break;
case JpgMarkers.DefineQuantizationTable:
skipData = false;
var specifications = QuantizationTableSpecification.ReadFromMarker(stream, strictMode);
foreach (var specification in specifications)
{
quantizationTables[specification.TableDestinationIdentifier] = specification;
}
break;
case JpgMarkers.DefineHuffmanTable:
skipData = false;
var huffmanTableSpecifications = HuffmanTableSpecification.ReadFromMarker(stream);
foreach (var specification in huffmanTableSpecifications)
{
if (specification.TableClass == HuffmanTableSpecification.HuffmanClass.DcTable)
{
dcHuffmanTables[specification.DestinationIdentifier] = HuffmanTable.FromSpecification(specification);
}
else
{
acHuffmanTables[specification.DestinationIdentifier] = HuffmanTable.FromSpecification(specification);
}
}
break;
case JpgMarkers.DefineArithmeticCodingConditioning:
throw new NotSupportedException("No support for arithmetic coding conditioning table yet.");
case JpgMarkers.DefineRestartInterval:
skipData = false;
// Specifies the length of this segment.
var restartIntervalSegmentLength = stream.ReadShort();
// Specifies the number of MCU in the restart interval.
var restartInterval = stream.ReadShort();
break;
case JpgMarkers.StartOfScan:
skipData = false;
var scanSingle = Scan.ReadFromMarker(stream, strictMode);
if (frames.Count == 0)
{
throw new InvalidOperationException("Scan encountered outside any frame.");
}
var frameForScan = frames[frames.Count - 1];
rgbData = new byte[3 * frameForScan.ImageHeight * frameForScan.ImageWidth];
frameForScan.Scans.Add(scanSingle);
if (frameForScan.FrameType != FrameType.BaselineHuffman)
{
throw new NotSupportedException($"No support for frame type: {frameForScan.FrameType}.");
}
ProcessScan(
rgbData,
frameForScan,
scanSingle,
quantizationTables,
dcHuffmanTables,
acHuffmanTables);
break;
case JpgMarkers.StartOfBaselineDctHuffmanFrame:
case JpgMarkers.StartOfExtendedSequentialDctHuffmanFrame:
case JpgMarkers.StartOfProgressiveDctHuffmanFrame:
case JpgMarkers.StartOfLosslessHuffmanFrame:
case JpgMarkers.StartOfDifferentialSequentialDctHuffmanFrame:
case JpgMarkers.StartOfDifferentialProgressiveDctHuffmanFrame:
case JpgMarkers.StartOfDifferentialLosslessHuffmanFrame:
case JpgMarkers.StartOfExtendedSequentialDctArithmeticFrame:
case JpgMarkers.StartOfProgressiveDctArithmeticFrame:
case JpgMarkers.StartOfLosslessArithmeticFrame:
case JpgMarkers.StartOfDifferentialSequentialDctArithmeticFrame:
case JpgMarkers.StartOfDifferentialProgressiveDctArithmeticFrame:
case JpgMarkers.StartOfDifferentialLosslessArithmeticFrame:
skipData = false;
var frame = Frame.ReadFromMarker(stream, strictMode, marker);
frames.Add(frame);
break;
}
marker = stream.ReadSegmentMarker(skipData, $"Expected next marker after reading section of type: {markerType}.");
markerType = (JpgMarkers)marker;
}
if (frames.Count == 0 || frames[frames.Count - 1].Scans.Count == 0)
{
throw new InvalidOperationException($"No image data found in the provided JPG.");
}
var result = frames[frames.Count - 1];
return new Jpg(result.ImageWidth, result.ImageHeight, rgbData, jfif, comments);
}
private static void ProcessScan(
byte[] resultHolder,
Frame frame,
Scan scan,
IReadOnlyDictionary<int, QuantizationTableSpecification> quantizationTables,
IReadOnlyDictionary<int, HuffmanTable> dcHuffmanTables,
IReadOnlyDictionary<int, HuffmanTable> acHuffmanTables)
{
var str = new BitStream(scan.Data);
// Y, Cb, Cr
var oldDcCoefficients = new int[frame.NumberOfComponents];
var samplesByComponent = new List<List<double[]>>();
var indexForResult = 0;
for (var row = 0; row < frame.McusPerY; row++)
{
for (var col = 0; col < frame.McusPerX; col++)
{
samplesByComponent.Clear();
for (var componentIndex = 0; componentIndex < frame.Components.Length; componentIndex++)
{
var componentSamples = new List<double[]>();
var component = frame.Components[componentIndex];
var qt = quantizationTables[component.DestinationQuantizationTableSelector];
var index = componentIndex > 0 ? 1 : 0;
for (var y = 0; y < component.HorizontalSamplingFactor; y++)
{
for (var x = 0; x < component.VerticalSamplingFactor; x++)
{
var (newDcCoeff, dataUnit) = DecodeDataUnit(
str,
index,
qt,
oldDcCoefficients[componentIndex],
dcHuffmanTables,
acHuffmanTables);
oldDcCoefficients[componentIndex] = newDcCoeff;
componentSamples.Add(dataUnit);
}
}
samplesByComponent.Add(componentSamples);
}
for (int y = 0; y < 8; y++)
{
if (y >= frame.ImageHeight)
{
break;
}
for (int x = 0; x < 8; x++)
{
if (x >= frame.ImageWidth)
{
break;
}
var flatIndex = (y * 8) + x;
var (r, g, b) = ToRgb(samplesByComponent[0][0][flatIndex],
samplesByComponent[1][0][flatIndex],
samplesByComponent[2][0][flatIndex]);
resultHolder[indexForResult++] = r;
resultHolder[indexForResult++] = g;
resultHolder[indexForResult++] = b;
}
}
}
}
}
private static (int dcCoefficient, double[] data) DecodeDataUnit(BitStream stream,
int index,
QuantizationTableSpecification quantization,
int previousDcCoefficient,
IReadOnlyDictionary<int, HuffmanTable> dcHuffmanTables,
IReadOnlyDictionary<int, HuffmanTable> acHuffmanTables)
{
// Each Minimum Coded Unit (MCU / 8*8 block) has 64 values, 1 DC and 63 AC coefficients.
// First up we get the DC coefficient, this is encoded as a difference from the DC coefficient in the previous MCU.
var table = dcHuffmanTables[index];
var category = table.Read(stream);
if (!category.HasValue)
{
throw new InvalidOperationException();
}
var value = stream.ReadNBits(category.Value);
var difference = JpgDecodeUtil.GetDcDifferenceOrAcCoefficient(category.Value, value);
var newDcCoefficient = previousDcCoefficient + difference;
var data = new double[64];
data[0] = newDcCoefficient * quantization.QuantizationTableElements[0];
var acHuffmanTable = acHuffmanTables[index];
// Now we decode the AC coefficients.
for (var i = 1; i < 64; i++)
{
/*
* AC coefficients are run-length encoded (RLE). The RLE data is then saved
* as the number of preceding zeros (RRRR) and the actual value (SSSS).
*/
var acCategoryRead = acHuffmanTable.Read(stream);
if (!acCategoryRead.HasValue)
{
throw new InvalidOperationException();
}
var acCategory = acCategoryRead.Value;
// The end-of-block (EOB) special marker, all remaining values are 0.
if (acCategory == 0b0000_0000)
{
break;
}
// The high 4 bits are the number of preceding values
if (acCategory > 0b0000_1111)
{
i += (acCategory >> 4);
acCategory = (byte)(acCategory & 0b0000_1111);
}
if (i > 63)
{
break;
}
var bits = stream.ReadNBits(acCategory);
var acCoefficient = JpgDecodeUtil.GetDcDifferenceOrAcCoefficient(acCategory, bits);
var acValue = acCoefficient * quantization.QuantizationTableElements[i];
var indexForValue = JpgDecodeUtil.ZigZagPattern[i];
data[indexForValue] = acValue;
}
var normalizingZeroFactor = 1 / Math.Sqrt(2);
var fullResult = new double[64];
for (int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{
// TODO: better IDCT https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/jddctmgr.c
// Or we could just cache most of this
var sum = 0d;
for (int u = 0; u < 8; u++)
{
var cu = u == 0 ? normalizingZeroFactor : 1;
for (int v = 0; v < 8; v++)
{
var cv = v == 0 ? normalizingZeroFactor : 1;
var valInner = cu * cv * data[(u * 8) + v]
* Math.Cos(((2 * x + 1) * u * Math.PI) / 16)
* Math.Cos(((2 * y + 1) * v * Math.PI) / 16);
sum += valInner;
}
}
// Transpose X and Y here
var resultIndex = (y) + (x * 8);
var pixelValue = Math.Round((0.25 * sum) + 128);
fullResult[resultIndex] = pixelValue;
}
}
return (newDcCoefficient, fullResult);
}
private static (byte, byte, byte) ToRgb(double y, double cb, double cr)
{
var crVal = cr - 128;
var cbVal = cb - 128;
var r = Math.Floor(y + (1.402 * crVal));
var g = Math.Floor(y - (0.34414 * cbVal) - (0.71414 * crVal));
var b = Math.Floor(y + (1.772 * cbVal));
r = r < 0 ? 0 : r > 255 ? 255 : r;
g = g < 0 ? 0 : g > 255 ? 255 : g;
b = b < 0 ? 0 : b > 255 ? 255 : b;
return ((byte)r, (byte)g, (byte)b);
}
public static bool HasJpgHeader(Stream stream)
{
var bytes = new byte[2];
var read = stream.Read(bytes, 0, 2);
if (read != 2)
{
return false;
}
return bytes[0] == MarkerStart
&& bytes[1] == StartOfImage;
}
}
}

View File

@ -0,0 +1,83 @@
namespace BigGustave.Jpgs
{
using System;
using System.IO;
internal static class JpgStreamReadExtensions
{
private const byte MarkerStart = 255;
private static readonly byte[] ShortBuffer = new byte[2];
private static readonly object ShortLock = new object();
public static short ReadShort(this Stream stream)
{
lock (ShortLock)
{
var read = stream.Read(ShortBuffer, 0, ShortBuffer.Length);
if (read != ShortBuffer.Length)
{
throw new InvalidOperationException();
}
// For parameters which are 2 bytes in length, the most significant byte shall come first
// in the compressed data's ordered sequence of bytes.
return (short)((ShortBuffer[0] << 8) + ShortBuffer[1]);
}
}
public static byte ReadByteActual(this Stream stream)
{
var val = stream.ReadByte();
if (val < 0)
{
throw new InvalidOperationException();
}
return (byte) val;
}
public static (byte, byte) ReadNibblePair(this Stream stream)
{
var b = ReadByteActual(stream);
// Parameters which are 4 bits in length always come in pairs, and the pair shall always be encoded in a single byte.
// The first 4-bit parameter of the pair shall occupy the most significant 4 bits of the byte.
return ((byte)(b >> 4), (byte)(b & 0x0F));
}
public static byte ReadSegmentMarker(this Stream stream, bool skipData = false, string message = null)
{
byte? previous = null;
int currentValue;
while ((currentValue = stream.ReadByte()) != -1)
{
var b = (byte)currentValue;
if (!skipData)
{
if (!previous.HasValue && b != MarkerStart)
{
throw new InvalidOperationException();
}
if (b != MarkerStart)
{
return b;
}
}
if (previous.HasValue && previous.Value == MarkerStart && b != MarkerStart)
{
return b;
}
previous = b;
}
throw new InvalidOperationException();
}
}
}

View File

@ -0,0 +1,21 @@
namespace BigGustave.Jpgs
{
/// <summary>
/// The units for pixel densities.
/// </summary>
public enum PixelDensityUnit : byte
{
/// <summary>
/// No units.
/// </summary>
None = 0,
/// <summary>
/// Pixels per inch.
/// </summary>
PixelsPerInch = 1,
/// <summary>
/// Pixels per centimeter.
/// </summary>
PixelsPerCm = 2
}
}

View File

@ -0,0 +1,111 @@
namespace BigGustave.Jpgs
{
using System;
using System.IO;
internal class QuantizationTableSpecification
{
/// <summary>
/// Offset from the start of the file to this table's marker.
/// </summary>
public long Offset { get; }
/// <summary>
/// Quantization table definition length Specifies the length of all quantization table parameters
/// </summary>
public short Length { get; }
/// <summary>
/// Specifies the bit-precision of quantization table elements.
/// Value 0 indicates 1 byte values.
/// Value 1 indicates 2 byte values.
/// </summary>
public byte ElementPrecision { get; }
/// <summary>
/// Specifies one of four possible destinations at the decoder into which the quantization table shall be installed.
/// </summary>
public byte TableDestinationIdentifier { get; }
/// <summary>
/// Specifies the 64 elements for the quantization table in zig-zag ordering.
/// </summary>
public short[] QuantizationTableElements { get; }
/// <summary>
/// Whether the elements in <see cref="QuantizationTableElements"/> are in 16-bit precision.
/// </summary>
public bool Uses16BitElements => ElementPrecision == 1;
public QuantizationTableSpecification(long offset, short length, byte elementPrecision,
byte tableDestinationIdentifier,
short[] quantizationTableElements)
{
Offset = offset;
Length = length;
ElementPrecision = elementPrecision;
TableDestinationIdentifier = tableDestinationIdentifier;
QuantizationTableElements = quantizationTableElements ?? throw new ArgumentNullException(nameof(quantizationTableElements));
if (elementPrecision > 1)
{
throw new ArgumentException($"Invalid element precision ({elementPrecision}) in quantization table at offset: {offset}.");
}
if (QuantizationTableElements.Length != 64)
{
throw new ArgumentException($"Invalid quantization table length, should be 64 but got: {QuantizationTableElements.Length}.");
}
}
/// <summary>
/// Reads the quantization table starting from the marker byte <see cref="JpgMarkers.DefineQuantizationTable"/>.
/// </summary>
public static QuantizationTableSpecification[] ReadFromMarker(Stream stream, bool strictMode)
{
var offset = stream.Position;
var length = stream.ReadShort();
// Section may contain multiple quantization tables, each with its own information byte.
var quantizationTableCount = length / 65;
var result = new QuantizationTableSpecification[quantizationTableCount];
for (var qtIndex = 0; qtIndex < quantizationTableCount; qtIndex++)
{
var (elementPrecision, destinationId) = stream.ReadNibblePair();
var uses16BitValues = false;
if (elementPrecision == 1)
{
uses16BitValues = true;
}
else if (elementPrecision != 0)
{
throw new InvalidOperationException(
"Invalid value for quantization table element precision, should be 0 or 1, " +
$"got: {elementPrecision} at offset {stream.Position}.");
}
var data = new short[64];
for (var i = 0; i < data.Length; i++)
{
data[i] = uses16BitValues ? stream.ReadShort() : stream.ReadByteActual();
}
result[qtIndex] = new QuantizationTableSpecification(offset, length, elementPrecision, destinationId, data);
}
var lengthRead = stream.Position - offset;
if (lengthRead != length && strictMode)
{
throw new InvalidOperationException($"Length read ({lengthRead}) for quantization table at offset {offset} " +
$"did not match length specified ({length}). Set strictMode to false to ignore this.");
}
return result;
}
}
}

View File

@ -0,0 +1,150 @@
using System.Collections.Generic;
namespace BigGustave.Jpgs
{
using System.IO;
internal class Scan
{
private readonly List<int> restartIndices;
public IReadOnlyList<byte> Data { get; }
public ComponentSpecificationParameters[] Components { get; }
public (byte start, byte end) SpectralPredictionSelection { get; }
public (byte high, byte low) SuccessiveApproximationBits { get; }
public Scan(ComponentSpecificationParameters[] components,
(byte start, byte end) spectralPredictionSelection,
(byte high, byte low) successiveApproximationBits,
List<byte> data,
List<int> restartIndices)
{
Components = components;
SpectralPredictionSelection = spectralPredictionSelection;
SuccessiveApproximationBits = successiveApproximationBits;
Data = data;
this.restartIndices = restartIndices;
}
public static Scan ReadFromMarker(Stream stream, bool strictMode)
{
// ReSharper disable once UnusedVariable
var length = stream.ReadShort();
var numberOfScanImageComponents = stream.ReadByteActual();
var componentSpecificationParameters = new ComponentSpecificationParameters[numberOfScanImageComponents];
for (var i = 0; i < numberOfScanImageComponents; i++)
{
var csj = stream.ReadByteActual();
var (tdj, taj) = stream.ReadNibblePair();
componentSpecificationParameters[i] = new ComponentSpecificationParameters(csj, tdj, taj);
}
var startOfSpectralOrPredictorSelection = stream.ReadByteActual();
var endOfSpectralSelection = stream.ReadByteActual();
var approximationBits = stream.ReadNibblePair();
// Read entropy-coded segment
// In new C# we could potentially use Spans here, depending on lifetime rules...
var data = new List<byte>();
var restartIndices = new List<int>();
byte lastByte = 0x00;
while (true)
{
var bi = stream.ReadByte();
if (bi < 0)
{
break;
}
var b = (byte) bi;
if (lastByte == 0xFF)
{
if (b == 0x00)
{
// Represents an 0xFF byte
data.Add(lastByte);
lastByte = 0;
continue;
}
if (b >= 0xD0 && b <= 0xD7)
{
// Restart markers
lastByte = 0x00;
restartIndices.Add(data.Count);
continue;
}
if (b == 0xFF)
{
// Fill bytes
lastByte = b;
continue;
}
stream.Seek(-2, SeekOrigin.Current);
break;
}
if (b == 0xFF)
{
lastByte = b;
continue;
}
data.Add(b);
lastByte = b;
}
return new Scan(componentSpecificationParameters,
(startOfSpectralOrPredictorSelection, endOfSpectralSelection),
approximationBits,
data, restartIndices);
}
public readonly struct ComponentSpecificationParameters
{
/// <summary>
/// Scan component selector - selects which of the image components specified in
/// the frame parameters shall be this component in the scan.
/// </summary>
public readonly byte ScanComponentSelector;
/// <summary>
/// Specifies 1 of 4 possible DC entropy coding table destinations
/// from which the entropy table needed for decoding the DC coefficients
/// of this component is retrieved.
/// </summary>
public readonly byte DcEntropyCodingTableDestinationSelector;
/// <summary>
/// Specifies 1 of 4 possible AC entropy coding table destinations
/// from which the entropy table needed for decoding the AC coefficients
/// of this component is retrieved.
/// </summary>
public readonly byte AcEntropyCodingTableDestinationSelector;
public ComponentSpecificationParameters(byte scanComponentSelector, byte dcEntropyCodingTableDestinationSelector, byte acEntropyCodingTableDestinationSelector)
{
ScanComponentSelector = scanComponentSelector;
DcEntropyCodingTableDestinationSelector = dcEntropyCodingTableDestinationSelector;
AcEntropyCodingTableDestinationSelector = acEntropyCodingTableDestinationSelector;
}
}
}
}