mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-06-28 15:30:17 +08:00
Add @EliotJones's BigGustave jpgs classes
This commit is contained in:
parent
8a82500427
commit
76c6e9436d
54
src/UglyToad.PdfPig/Filters/Jpgs/BitStream.cs
Normal file
54
src/UglyToad.PdfPig/Filters/Jpgs/BitStream.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
45
src/UglyToad.PdfPig/Filters/Jpgs/Comment.cs
Normal file
45
src/UglyToad.PdfPig/Filters/Jpgs/Comment.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
177
src/UglyToad.PdfPig/Filters/Jpgs/Frame.cs
Normal file
177
src/UglyToad.PdfPig/Filters/Jpgs/Frame.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
src/UglyToad.PdfPig/Filters/Jpgs/FrameType.cs
Normal file
19
src/UglyToad.PdfPig/Filters/Jpgs/FrameType.cs
Normal 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,
|
||||
}
|
||||
}
|
17
src/UglyToad.PdfPig/Filters/Jpgs/FrameTypeExtensions.cs
Normal file
17
src/UglyToad.PdfPig/Filters/Jpgs/FrameTypeExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
179
src/UglyToad.PdfPig/Filters/Jpgs/HuffmanTable.cs
Normal file
179
src/UglyToad.PdfPig/Filters/Jpgs/HuffmanTable.cs
Normal 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}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
116
src/UglyToad.PdfPig/Filters/Jpgs/Jfif.cs
Normal file
116
src/UglyToad.PdfPig/Filters/Jpgs/Jfif.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
87
src/UglyToad.PdfPig/Filters/Jpgs/Jpg.cs
Normal file
87
src/UglyToad.PdfPig/Filters/Jpgs/Jpg.cs
Normal 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);
|
||||
}
|
||||
}
|
42
src/UglyToad.PdfPig/Filters/Jpgs/JpgDecodeUtil.cs
Normal file
42
src/UglyToad.PdfPig/Filters/Jpgs/JpgDecodeUtil.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
101
src/UglyToad.PdfPig/Filters/Jpgs/JpgMarkers.cs
Normal file
101
src/UglyToad.PdfPig/Filters/Jpgs/JpgMarkers.cs
Normal 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
|
||||
}
|
||||
}
|
375
src/UglyToad.PdfPig/Filters/Jpgs/JpgOpener.cs
Normal file
375
src/UglyToad.PdfPig/Filters/Jpgs/JpgOpener.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
83
src/UglyToad.PdfPig/Filters/Jpgs/JpgStreamReadExtensions.cs
Normal file
83
src/UglyToad.PdfPig/Filters/Jpgs/JpgStreamReadExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
21
src/UglyToad.PdfPig/Filters/Jpgs/PixelDensityUnit.cs
Normal file
21
src/UglyToad.PdfPig/Filters/Jpgs/PixelDensityUnit.cs
Normal 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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
150
src/UglyToad.PdfPig/Filters/Jpgs/Scan.cs
Normal file
150
src/UglyToad.PdfPig/Filters/Jpgs/Scan.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user