Skip to content

Commit

Permalink
Jpeg reading / writing is almost complete.
Browse files Browse the repository at this point in the history
Can't wait for entropy decoding / encoding...

Need sleep.
  • Loading branch information
juliusfriedman committed Oct 22, 2024
1 parent a98c717 commit 4dd04cb
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 75 deletions.
4 changes: 2 additions & 2 deletions Codecs/Image/Jpeg/Classes/FrameComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Codec.Jpeg.Classes;

public class FrameComponent : MemorySegment
public sealed class FrameComponent : MemorySegment
{
/// <summary>
/// The length of a <see cref="FrameComponent"/> in bytes.
Expand All @@ -25,7 +25,7 @@ public int HorizontalSamplingFactor
{
get
{
var bitOffset= Binary.BytesToBits(Offset + 1);
var bitOffset = Binary.BytesToBits(Offset + 1);
return (int)this.ReadBits(ref bitOffset, Binary.Four, Binary.BitOrder.MostSignificant);
}
set
Expand Down
98 changes: 98 additions & 0 deletions Codecs/Image/Jpeg/Classes/JpegState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Codec.Jpeg.Markers;
using System;

namespace Codec.Jpeg.Classes;

/// <summary>
/// Contains useful information about the current state of the Jpeg image.
/// </summary>
internal sealed class JpegState : IEquatable<JpegState>
{
/// <summary>
/// The function code which corresponds to the StartOfScan marker.
/// </summary>
public byte StartOfFrameFunctionCode;

/// <summary>
/// Start of spectral or predictor selection
/// </summary>
public byte Ss;

/// <summary>
/// End of spectral selection
/// </summary>
public byte Se;

/// <summary>
/// Successive approximation bit position high
/// </summary>
public byte Ah;

/// <summary>
/// Successive approximation bit position low or point transform
/// </summary>
public byte Al;

/// <summary>
/// Constructors a <see cref="JpegState"/>
/// </summary>
/// <param name="startOfScanFunctionCode"></param>
/// <param name="ss"></param>
/// <param name="se"></param>
/// <param name="ah"></param>
/// <param name="al"></param>
public JpegState(byte startOfScanFunctionCode, byte ss, byte se, byte ah, byte al)
{
StartOfFrameFunctionCode = startOfScanFunctionCode;
Ss = ss;
Se = se;
Ah = ah;
Al = al;
}

/// <summary>
/// Constructs a <see cref="JpegState"/>
/// </summary>
/// <param name="data"></param>
public JpegState(StartOfScan sos)
{
StartOfFrameFunctionCode = sos.FunctionCode;
Ss = (byte)sos.Ss;
Se = (byte)sos.Se;
Ah = (byte)sos.Ah;
Al = (byte)sos.Al;
}


/// <summary>
/// Copy constructor
/// </summary>
/// <param name="other"></param>
public JpegState (JpegState other)
{
StartOfFrameFunctionCode = other.StartOfFrameFunctionCode;
Ss = other.Ss;
Se = other.Se;
Ah = other.Ah;
Al = other.Al;
}

public override bool Equals(object obj)
=> obj is JpegState jpegState && Equals(jpegState);

public bool Equals(JpegState other)
=> StartOfFrameFunctionCode == other.StartOfFrameFunctionCode &&
Ss == other.Ss &&
Se == other.Se &&
Ah == other.Ah &&
Al == other.Al;

public override int GetHashCode()
=> HashCode.Combine(StartOfFrameFunctionCode, Ss, Se, Ah, Al);

public static bool operator ==(JpegState a, JpegState b)
=> a.Equals(b);

public static bool operator !=(JpegState a, JpegState b)
=> false == a.Equals(b);
}
12 changes: 4 additions & 8 deletions Codecs/Image/Jpeg/Classes/ScanComponentSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@ public byte Tdj
{
get
{
using var slice = RawData;
var bitOffset = Binary.BytesToBits(Offset + 1);
return (byte)slice.ReadBits(bitOffset, Binary.Four, Binary.BitOrder.MostSignificant);
return (byte)this.ReadBits(bitOffset, Binary.Four, Binary.BitOrder.MostSignificant);
}
set
{
using var slice = RawData;
var bitOffset = Binary.BytesToBits(Offset + 1);
slice.WriteBits(bitOffset, Binary.Four, value, Binary.BitOrder.MostSignificant);
this.WriteBits(bitOffset, Binary.Four, value, Binary.BitOrder.MostSignificant);
}
}

Expand All @@ -44,15 +42,13 @@ public byte Taj
{
get
{
using var slice = RawData;
var bitOffset = Binary.BytesToBits(Offset + 1) + Binary.Four;
return (byte)slice.ReadBits(bitOffset, Binary.Four, Binary.BitOrder.MostSignificant);
return (byte)this.ReadBits(bitOffset, Binary.Four, Binary.BitOrder.MostSignificant);
}
set
{
using var slice = RawData;
var bitOffset = Binary.BytesToBits(Offset + 1) + Binary.Four;
slice.WriteBits(bitOffset, Binary.Four, value, Binary.BitOrder.MostSignificant);
this.WriteBits(bitOffset, Binary.Four, value, Binary.BitOrder.MostSignificant);
}
}

Expand Down
13 changes: 13 additions & 0 deletions Codecs/Image/Jpeg/JpegCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ public class JpegCodec : ImageCodec, IEncoder, IDecoder
{
const int ComponentCount = 3;

public static ImageFormat DefaultImageFormat
{
get => new
(
Binary.ByteOrder.Big,
DataLayout.Packed,
new JpegComponent(0, 1, 8),
new JpegComponent(0, 2, 8),
new JpegComponent(0, 3, 8),
new JpegComponent(0, 4, 8)
);
}

public JpegCodec()
: base("JPEG", Binary.ByteOrder.Little, ComponentCount, Binary.BitsPerByte)
{
Expand Down
4 changes: 1 addition & 3 deletions Codecs/Image/Jpeg/JpegComponent.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Media.Codec;

namespace Codec.Jpeg;
namespace Media.Codec.Jpeg;

public class JpegComponent : MediaComponent
{
Expand Down
71 changes: 40 additions & 31 deletions Codecs/Image/Jpeg/JpegImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@
using Media.Common;
using System.Linq;
using Media.Common.Collections.Generic;
using Codec.Jpeg;
using Codec.Jpeg.Markers;
using Codec.Jpeg.Classes;

namespace Media.Codec.Jpeg;

public class JpegImage : Image
{
public readonly bool Progressive;
internal readonly JpegState JpegState;
public readonly ConcurrentThesaurus<byte, Marker> Markers;

public JpegImage(ImageFormat imageFormat, int width, int height)
: base(imageFormat, width, height, new JpegCodec())
{
JpegState = new JpegState(Jpeg.Markers.StartOfBaselineFrame, 0, 63, 0, 0);
}

private JpegImage(ImageFormat imageFormat, int width, int height, MemorySegment data, bool progressive, ConcurrentThesaurus<byte, Marker> markers)
private JpegImage(ImageFormat imageFormat, int width, int height, MemorySegment data, JpegState jpegState, ConcurrentThesaurus<byte, Marker> markers)
: base(imageFormat, width, height, data, new JpegCodec())
{
Progressive = progressive;
JpegState = jpegState;
Markers = markers;
}

Expand All @@ -34,7 +34,7 @@ public static JpegImage FromStream(Stream stream)
ImageFormat imageFormat = default;
MemorySegment dataSegment = default;
MemorySegment thumbnailData = default;
bool progressive = false;
JpegState jpegState = new(Jpeg.Markers.Prefix, 0, 63, 0, 0);
ConcurrentThesaurus<byte, Marker> markers = new ConcurrentThesaurus<byte, Marker>();
foreach (var marker in JpegCodec.ReadMarkers(stream))
{
Expand All @@ -57,16 +57,8 @@ public static JpegImage FromStream(Stream stream)
case Jpeg.Markers.StartOfProgressiveArithmeticFrame:
case Jpeg.Markers.StartOfProgressiveHuffmanFrame:
case Jpeg.Markers.HeirarchicalProgression:
switch (marker.FunctionCode)
{
case Jpeg.Markers.StartOfDifferentialProgressiveHuffmanFrame:
case Jpeg.Markers.StartOfDifferentialProgressiveArithmeticFrame:
case Jpeg.Markers.StartOfProgressiveArithmeticFrame:
case Jpeg.Markers.StartOfProgressiveHuffmanFrame:
case Jpeg.Markers.HeirarchicalProgression:
progressive = true;
break;
}

jpegState.StartOfFrameFunctionCode = marker.FunctionCode;

StartOfFrame tag;

Expand All @@ -77,9 +69,9 @@ public static JpegImage FromStream(Stream stream)
else
{
tag = new StartOfFrame(marker);
}
}

int bitDepth = Binary.Clamp(Binary.BitsPerByte, Binary.BitsPerInteger, tag.P);
int bitDepth = Binary.Clamp(tag.P, Binary.BitsPerByte, Binary.BitsPerInteger);
height = tag.Y;
width = tag.X;

Expand Down Expand Up @@ -115,17 +107,33 @@ public static JpegImage FromStream(Stream stream)
}

// Create the image format based on the SOF0 data
imageFormat = new ImageFormat(Binary.ByteOrder.Little, DataLayout.Planar, mediaComponents);
imageFormat = new ImageFormat(Binary.ByteOrder.Big, DataLayout.Planar, mediaComponents);
imageFormat.HorizontalSamplingFactors = widths;
imageFormat.VerticalSamplingFactors = heights;
tag.Dispose();
tag = null;
continue;
case Jpeg.Markers.StartOfScan:
{
using var sos = new StartOfScan(marker);
using var sos = new StartOfScan(marker);

jpegState.Ss = (byte)sos.Ss;

if (sos.Se > 0)
jpegState.Se = (byte)sos.Se;

jpegState.Ah = (byte)sos.Ah;
jpegState.Al = (byte)sos.Al;

if (jpegState.StartOfFrameFunctionCode == Jpeg.Markers.StartOfProgressiveHuffmanFrame)
jpegState.Al = 1;

for(int ns = sos.Ns, i = 0; i < ns; ++i)
if (imageFormat == null)
imageFormat = JpegCodec.DefaultImageFormat;

///If Ns > 1, the following restriction shall be placed on the image components contained in the scan:
///(The summation of all components products of thier respective values correspond to 10.
for (int ns = Binary.Min(4, sos.Ns), i = 0; i < ns; ++i)
{
using var scanComponentSelector = sos[i];
var jpegComponent = imageFormat.GetComponentById(scanComponentSelector.Csj) as JpegComponent ?? imageFormat.Components[i] as JpegComponent;
Expand All @@ -138,6 +146,7 @@ public static JpegImage FromStream(Stream stream)
var read = stream.Read(dataSegment.Array, dataSegment.Offset, dataSegment.Count);
if (read < dataSegment.Count)
dataSegment = dataSegment.Slice(0, read);

break;
}
case Jpeg.Markers.AppFirst:
Expand Down Expand Up @@ -171,8 +180,8 @@ public static JpegImage FromStream(Stream stream)
if (imageFormat == null || dataSegment == null && thumbnailData == null)
throw new InvalidDataException("The provided stream does not contain valid JPEG image data.");

// Create and return the JpegImage
return new JpegImage(imageFormat, width, height, dataSegment ?? thumbnailData, progressive, markers);
// Create and return the JpegImage (Could use a Default Jpeg State?)
return new JpegImage(imageFormat, width, height, dataSegment ?? thumbnailData, jpegState, markers);
}

public void Save(Stream stream)
Expand Down Expand Up @@ -206,9 +215,8 @@ public void Save(Stream stream)
}
}

// TODO revise to write correct start of scan header per coding.
// Write the SOF marker
WriteStartOfFrame(Progressive ? Jpeg.Markers.StartOfProgressiveHuffmanFrame : Jpeg.Markers.StartOfBaselineFrame, stream);
WriteStartOfFrame(JpegState.StartOfFrameFunctionCode, stream);

if (markerBuffer != null)
{
Expand Down Expand Up @@ -268,15 +276,16 @@ private void WriteStartOfFrame(byte functionCode, Stream stream)
}

private void WriteStartOfScan(Stream stream)
{
{
var numberOfComponents = ImageFormat.Components.Length;

using var sos = new StartOfScan(numberOfComponents);
// Set the Ss, Se, Ah, and Al fields
// These values are typical for a baseline or progessive JPEG but not lossless. (1-7)
sos.Ss = 0; // Start of spectral selection
sos.Se = 63; // End of spectral selection
sos.Ah = 0; // Successive approximation high
sos.Al = 0; // Successive approximation low

sos.Ss = JpegState.Ss;
sos.Se = JpegState.Se;
sos.Ah = JpegState.Ah;
sos.Al = JpegState.Al;

for (var i = 0; i < numberOfComponents; ++i)
{
var imageComponent = ImageFormat.Components[i];
Expand Down
34 changes: 18 additions & 16 deletions Codecs/Image/Jpeg/JpegUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,25 +250,27 @@ public static void TestLoad()
jpgImage.Save(outputNew);
}

using var inputNew = new FileStream(saveFileName, FileMode.OpenOrCreate, FileAccess.Read);

var destinationMarkers = new List<Marker>();

foreach (var marker in JpegCodec.ReadMarkers(inputNew))
using (var inputNew = new FileStream(saveFileName, FileMode.OpenOrCreate, FileAccess.Read))
{
DumpMarker(marker);
}

inputNew.Seek(0, SeekOrigin.Begin);
var destinationMarkers = new List<Marker>();

foreach (var marker in JpegCodec.ReadMarkers(inputNew))
{
DumpMarker(marker);
}

using var newJpgImage = JpegImage.FromStream(inputNew);
inputNew.Seek(0, SeekOrigin.Begin);

if (newJpgImage.Width != jpgImage.Width ||
newJpgImage.Height != jpgImage.Height ||
newJpgImage.Progressive != jpgImage.Progressive ||
newJpgImage.ImageFormat.Components.Length != jpgImage.ImageFormat.Components.Length ||
newJpgImage.ImageFormat.Size != jpgImage.ImageFormat.Size)
throw new InvalidDataException();
using (var newJpgImage = JpegImage.FromStream(inputNew))
{
if (newJpgImage.Width != jpgImage.Width ||
newJpgImage.Height != jpgImage.Height ||
newJpgImage.JpegState != jpgImage.JpegState ||
newJpgImage.ImageFormat.Components.Length != jpgImage.ImageFormat.Components.Length ||
newJpgImage.ImageFormat.Size != jpgImage.ImageFormat.Size)
throw new InvalidDataException();
}
}
}
}
}
2 changes: 1 addition & 1 deletion Codecs/Image/Jpeg/Marker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public int Length

public int MarkerLength => DataLength + 2;

public int DataOffset => Offset + PrefixBytes + LengthBytes;
public int DataOffset => DataLength > 0 ? Offset + PrefixBytes + LengthBytes : Count - 1;

public MemorySegment Data => Count > PrefixBytes + LengthBytes ? this.Slice(PrefixBytes + LengthBytes) : Empty;

Expand Down
Loading

0 comments on commit 4dd04cb

Please sign in to comment.