|
|
|
@ -25,11 +25,6 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// </summary>
|
|
|
|
private static readonly JpegFrameConfig[] FrameConfigs = CreateFrameConfigs(); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A scratch buffer to reduce allocations.
|
|
|
|
/// </summary>
|
|
|
|
private readonly byte[] buffer = new byte[20]; |
|
|
|
|
|
|
|
private readonly JpegEncoder encoder; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -67,6 +62,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
cancellationToken.ThrowIfCancellationRequested(); |
|
|
|
|
|
|
|
this.outputStream = stream; |
|
|
|
Span<byte> buffer = stackalloc byte[20]; |
|
|
|
|
|
|
|
ImageMetadata metadata = image.Metadata; |
|
|
|
JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); |
|
|
|
@ -76,39 +72,39 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
using JpegFrame frame = new(image, frameConfig, interleaved); |
|
|
|
|
|
|
|
// Write the Start Of Image marker.
|
|
|
|
this.WriteStartOfImage(); |
|
|
|
this.WriteStartOfImage(buffer); |
|
|
|
|
|
|
|
// Write APP0 marker
|
|
|
|
if (frameConfig.AdobeColorTransformMarkerFlag is null) |
|
|
|
{ |
|
|
|
this.WriteJfifApplicationHeader(metadata); |
|
|
|
this.WriteJfifApplicationHeader(metadata, buffer); |
|
|
|
} |
|
|
|
|
|
|
|
// Write APP14 marker with adobe color extension
|
|
|
|
else |
|
|
|
{ |
|
|
|
this.WriteApp14Marker(frameConfig.AdobeColorTransformMarkerFlag.Value); |
|
|
|
this.WriteApp14Marker(frameConfig.AdobeColorTransformMarkerFlag.Value, buffer); |
|
|
|
} |
|
|
|
|
|
|
|
// Write Exif, XMP, ICC and IPTC profiles
|
|
|
|
this.WriteProfiles(metadata); |
|
|
|
this.WriteProfiles(metadata, buffer); |
|
|
|
|
|
|
|
// Write the image dimensions.
|
|
|
|
this.WriteStartOfFrame(image.Width, image.Height, frameConfig); |
|
|
|
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer); |
|
|
|
|
|
|
|
// Write the Huffman tables.
|
|
|
|
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream); |
|
|
|
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder); |
|
|
|
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer); |
|
|
|
|
|
|
|
// Write the quantization tables.
|
|
|
|
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata); |
|
|
|
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer); |
|
|
|
|
|
|
|
// Write scans with actual pixel data
|
|
|
|
using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables); |
|
|
|
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, cancellationToken); |
|
|
|
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); |
|
|
|
|
|
|
|
// Write the End Of Image marker.
|
|
|
|
this.WriteEndOfImageMarker(); |
|
|
|
this.WriteEndOfImageMarker(buffer); |
|
|
|
|
|
|
|
stream.Flush(); |
|
|
|
} |
|
|
|
@ -116,58 +112,59 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// <summary>
|
|
|
|
/// Write the start of image marker.
|
|
|
|
/// </summary>
|
|
|
|
private void WriteStartOfImage() |
|
|
|
private void WriteStartOfImage(Span<byte> buffer) |
|
|
|
{ |
|
|
|
// Markers are always prefixed with 0xff.
|
|
|
|
this.buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
this.buffer[1] = JpegConstants.Markers.SOI; |
|
|
|
buffer[1] = JpegConstants.Markers.SOI; |
|
|
|
buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
|
|
|
|
this.outputStream.Write(this.buffer, 0, 2); |
|
|
|
this.outputStream.Write(buffer, 0, 2); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Writes the application header containing the JFIF identifier plus extra data.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="meta">The image metadata.</param>
|
|
|
|
private void WriteJfifApplicationHeader(ImageMetadata meta) |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteJfifApplicationHeader(ImageMetadata meta, Span<byte> buffer) |
|
|
|
{ |
|
|
|
// Write the JFIF headers
|
|
|
|
this.buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
this.buffer[1] = JpegConstants.Markers.APP0; // Application Marker
|
|
|
|
this.buffer[2] = 0x00; |
|
|
|
this.buffer[3] = 0x10; |
|
|
|
this.buffer[4] = 0x4a; // J
|
|
|
|
this.buffer[5] = 0x46; // F
|
|
|
|
this.buffer[6] = 0x49; // I
|
|
|
|
this.buffer[7] = 0x46; // F
|
|
|
|
this.buffer[8] = 0x00; // = "JFIF",'\0'
|
|
|
|
this.buffer[9] = 0x01; // versionhi
|
|
|
|
this.buffer[10] = 0x01; // versionlo
|
|
|
|
// Write the JFIF headers (highest index first to avoid additional bound checks)
|
|
|
|
buffer[10] = 0x01; // versionlo
|
|
|
|
buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
buffer[1] = JpegConstants.Markers.APP0; // Application Marker
|
|
|
|
buffer[2] = 0x00; |
|
|
|
buffer[3] = 0x10; |
|
|
|
buffer[4] = 0x4a; // J
|
|
|
|
buffer[5] = 0x46; // F
|
|
|
|
buffer[6] = 0x49; // I
|
|
|
|
buffer[7] = 0x46; // F
|
|
|
|
buffer[8] = 0x00; // = "JFIF",'\0'
|
|
|
|
buffer[9] = 0x01; // versionhi
|
|
|
|
|
|
|
|
// Resolution. Big Endian
|
|
|
|
Span<byte> hResolution = this.buffer.AsSpan(12, 2); |
|
|
|
Span<byte> vResolution = this.buffer.AsSpan(14, 2); |
|
|
|
Span<byte> hResolution = buffer.Slice(12, 2); |
|
|
|
Span<byte> vResolution = buffer.Slice(14, 2); |
|
|
|
|
|
|
|
if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter) |
|
|
|
{ |
|
|
|
// Scale down to PPI
|
|
|
|
this.buffer[11] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits
|
|
|
|
buffer[11] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits
|
|
|
|
BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.HorizontalResolution))); |
|
|
|
BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution))); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// We can simply pass the value.
|
|
|
|
this.buffer[11] = (byte)meta.ResolutionUnits; // xyunits
|
|
|
|
buffer[11] = (byte)meta.ResolutionUnits; // xyunits
|
|
|
|
BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(meta.HorizontalResolution)); |
|
|
|
BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution)); |
|
|
|
} |
|
|
|
|
|
|
|
// No thumbnail
|
|
|
|
this.buffer[16] = 0x00; // Thumbnail width
|
|
|
|
this.buffer[17] = 0x00; // Thumbnail height
|
|
|
|
buffer[17] = 0x00; // Thumbnail height
|
|
|
|
buffer[16] = 0x00; // Thumbnail width
|
|
|
|
|
|
|
|
this.outputStream.Write(this.buffer, 0, 18); |
|
|
|
this.outputStream.Write(buffer, 0, 18); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -175,8 +172,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// </summary>
|
|
|
|
/// <param name="tableConfigs">The table configuration.</param>
|
|
|
|
/// <param name="scanEncoder">The scan encoder.</param>
|
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
/// <exception cref="ArgumentNullException"><paramref name="tableConfigs"/> is <see langword="null"/>.</exception>
|
|
|
|
private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder) |
|
|
|
private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder, Span<byte> buffer) |
|
|
|
{ |
|
|
|
if (tableConfigs is null) |
|
|
|
{ |
|
|
|
@ -190,7 +188,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
markerlen += 1 + 16 + tableConfigs[i].Table.Values.Length; |
|
|
|
} |
|
|
|
|
|
|
|
this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen); |
|
|
|
this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen, buffer); |
|
|
|
for (int i = 0; i < tableConfigs.Length; i++) |
|
|
|
{ |
|
|
|
JpegHuffmanTableConfig tableConfig = tableConfigs[i]; |
|
|
|
@ -208,37 +206,39 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// Writes the APP14 marker to indicate the image is in RGB color space.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="colorTransform">The color transform byte.</param>
|
|
|
|
private void WriteApp14Marker(byte colorTransform) |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteApp14Marker(byte colorTransform, Span<byte> buffer) |
|
|
|
{ |
|
|
|
this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length); |
|
|
|
this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length, buffer); |
|
|
|
|
|
|
|
// Identifier: ASCII "Adobe".
|
|
|
|
this.buffer[0] = 0x41; |
|
|
|
this.buffer[1] = 0x64; |
|
|
|
this.buffer[2] = 0x6F; |
|
|
|
this.buffer[3] = 0x62; |
|
|
|
this.buffer[4] = 0x65; |
|
|
|
// Identifier: ASCII "Adobe" (highest index first to avoid additional bound checks).
|
|
|
|
buffer[4] = 0x65; |
|
|
|
buffer[0] = 0x41; |
|
|
|
buffer[1] = 0x64; |
|
|
|
buffer[2] = 0x6F; |
|
|
|
buffer[3] = 0x62; |
|
|
|
|
|
|
|
// Version, currently 100.
|
|
|
|
BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(5, 2), 100); |
|
|
|
BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(5, 2), 100); |
|
|
|
|
|
|
|
// Flags0
|
|
|
|
BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(7, 2), 0); |
|
|
|
BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(7, 2), 0); |
|
|
|
|
|
|
|
// Flags1
|
|
|
|
BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(9, 2), 0); |
|
|
|
BinaryPrimitives.WriteInt16BigEndian(buffer.Slice(9, 2), 0); |
|
|
|
|
|
|
|
// Color transform byte
|
|
|
|
this.buffer[11] = colorTransform; |
|
|
|
buffer[11] = colorTransform; |
|
|
|
|
|
|
|
this.outputStream.Write(this.buffer.AsSpan(0, 12)); |
|
|
|
this.outputStream.Write(buffer.Slice(0, 12)); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Writes the EXIF profile.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="exifProfile">The exif profile.</param>
|
|
|
|
private void WriteExifProfile(ExifProfile exifProfile) |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteExifProfile(ExifProfile exifProfile, Span<byte> buffer) |
|
|
|
{ |
|
|
|
if (exifProfile is null || exifProfile.Values.Count == 0) |
|
|
|
{ |
|
|
|
@ -262,7 +262,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
int app1Length = bytesToWrite + 2; |
|
|
|
|
|
|
|
// Write the app marker, EXIF marker, and data
|
|
|
|
this.WriteApp1Header(app1Length); |
|
|
|
this.WriteApp1Header(app1Length, buffer); |
|
|
|
this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker); |
|
|
|
this.outputStream.Write(data, 0, bytesToWrite - exifMarkerLength); |
|
|
|
remaining -= bytesToWrite; |
|
|
|
@ -273,7 +273,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
bytesToWrite = remaining > maxBytesWithExifId ? maxBytesWithExifId : remaining; |
|
|
|
app1Length = bytesToWrite + 2 + exifMarkerLength; |
|
|
|
|
|
|
|
this.WriteApp1Header(app1Length); |
|
|
|
this.WriteApp1Header(app1Length, buffer); |
|
|
|
|
|
|
|
// Write Exif00 marker
|
|
|
|
this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker); |
|
|
|
@ -289,10 +289,11 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// Writes the IPTC metadata.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="iptcProfile">The iptc metadata to write.</param>
|
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
/// <exception cref="ImageFormatException">
|
|
|
|
/// Thrown if the IPTC profile size exceeds the limit of 65533 bytes.
|
|
|
|
/// </exception>
|
|
|
|
private void WriteIptcProfile(IptcProfile iptcProfile) |
|
|
|
private void WriteIptcProfile(IptcProfile iptcProfile, Span<byte> buffer) |
|
|
|
{ |
|
|
|
const int maxBytes = 65533; |
|
|
|
if (iptcProfile is null || !iptcProfile.Values.Any()) |
|
|
|
@ -316,14 +317,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker.Length + |
|
|
|
Components.Decoder.ProfileResolver.AdobeIptcMarker.Length + |
|
|
|
2 + 4 + data.Length; |
|
|
|
this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13); |
|
|
|
this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13, buffer); |
|
|
|
this.outputStream.Write(Components.Decoder.ProfileResolver.AdobePhotoshopApp13Marker); |
|
|
|
this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker); |
|
|
|
this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeIptcMarker); |
|
|
|
this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even)
|
|
|
|
this.outputStream.WriteByte(0); |
|
|
|
BinaryPrimitives.WriteInt32BigEndian(this.buffer, data.Length); |
|
|
|
this.outputStream.Write(this.buffer, 0, 4); |
|
|
|
BinaryPrimitives.WriteInt32BigEndian(buffer, data.Length); |
|
|
|
this.outputStream.Write(buffer, 0, 4); |
|
|
|
this.outputStream.Write(data, 0, data.Length); |
|
|
|
} |
|
|
|
|
|
|
|
@ -331,10 +332,11 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// Writes the XMP metadata.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="xmpProfile">The XMP metadata to write.</param>
|
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
/// <exception cref="ImageFormatException">
|
|
|
|
/// Thrown if the XMP profile size exceeds the limit of 65533 bytes.
|
|
|
|
/// </exception>
|
|
|
|
private void WriteXmpProfile(XmpProfile xmpProfile) |
|
|
|
private void WriteXmpProfile(XmpProfile xmpProfile, Span<byte> buffer) |
|
|
|
{ |
|
|
|
if (xmpProfile is null) |
|
|
|
{ |
|
|
|
@ -367,7 +369,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
dataLength -= length; |
|
|
|
|
|
|
|
int app1Length = 2 + Components.Decoder.ProfileResolver.XmpMarker.Length + length; |
|
|
|
this.WriteApp1Header(app1Length); |
|
|
|
this.WriteApp1Header(app1Length, buffer); |
|
|
|
this.outputStream.Write(Components.Decoder.ProfileResolver.XmpMarker); |
|
|
|
this.outputStream.Write(data, offset, length); |
|
|
|
|
|
|
|
@ -379,32 +381,35 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// Writes the App1 header.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="app1Length">The length of the data the app1 marker contains.</param>
|
|
|
|
private void WriteApp1Header(int app1Length) |
|
|
|
=> this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1); |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteApp1Header(int app1Length, Span<byte> buffer) |
|
|
|
=> this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1, buffer); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Writes a AppX header.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="length">The length of the data the app marker contains.</param>
|
|
|
|
/// <param name="appMarker">The app marker to write.</param>
|
|
|
|
private void WriteAppHeader(int length, byte appMarker) |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteAppHeader(int length, byte appMarker, Span<byte> buffer) |
|
|
|
{ |
|
|
|
this.buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
this.buffer[1] = appMarker; |
|
|
|
this.buffer[2] = (byte)((length >> 8) & 0xFF); |
|
|
|
this.buffer[3] = (byte)(length & 0xFF); |
|
|
|
buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
buffer[1] = appMarker; |
|
|
|
buffer[2] = (byte)((length >> 8) & 0xFF); |
|
|
|
buffer[3] = (byte)(length & 0xFF); |
|
|
|
|
|
|
|
this.outputStream.Write(this.buffer, 0, 4); |
|
|
|
this.outputStream.Write(buffer, 0, 4); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Writes the ICC profile.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="iccProfile">The ICC profile to write.</param>
|
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
/// <exception cref="ImageFormatException">
|
|
|
|
/// Thrown if any of the ICC profiles size exceeds the limit.
|
|
|
|
/// </exception>
|
|
|
|
private void WriteIccProfile(IccProfile iccProfile) |
|
|
|
private void WriteIccProfile(IccProfile iccProfile, Span<byte> buffer) |
|
|
|
{ |
|
|
|
if (iccProfile is null) |
|
|
|
{ |
|
|
|
@ -446,30 +451,31 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
|
|
|
|
dataLength -= length; |
|
|
|
|
|
|
|
this.buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
this.buffer[1] = JpegConstants.Markers.APP2; // Application Marker
|
|
|
|
buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
buffer[1] = JpegConstants.Markers.APP2; // Application Marker
|
|
|
|
int markerLength = length + 16; |
|
|
|
this.buffer[2] = (byte)((markerLength >> 8) & 0xFF); |
|
|
|
this.buffer[3] = (byte)(markerLength & 0xFF); |
|
|
|
|
|
|
|
this.outputStream.Write(this.buffer, 0, 4); |
|
|
|
|
|
|
|
this.buffer[0] = (byte)'I'; |
|
|
|
this.buffer[1] = (byte)'C'; |
|
|
|
this.buffer[2] = (byte)'C'; |
|
|
|
this.buffer[3] = (byte)'_'; |
|
|
|
this.buffer[4] = (byte)'P'; |
|
|
|
this.buffer[5] = (byte)'R'; |
|
|
|
this.buffer[6] = (byte)'O'; |
|
|
|
this.buffer[7] = (byte)'F'; |
|
|
|
this.buffer[8] = (byte)'I'; |
|
|
|
this.buffer[9] = (byte)'L'; |
|
|
|
this.buffer[10] = (byte)'E'; |
|
|
|
this.buffer[11] = 0x00; |
|
|
|
this.buffer[12] = (byte)current; // The position within the collection.
|
|
|
|
this.buffer[13] = (byte)count; // The total number of profiles.
|
|
|
|
|
|
|
|
this.outputStream.Write(this.buffer, 0, iccOverheadLength); |
|
|
|
buffer[2] = (byte)((markerLength >> 8) & 0xFF); |
|
|
|
buffer[3] = (byte)(markerLength & 0xFF); |
|
|
|
|
|
|
|
this.outputStream.Write(buffer, 0, 4); |
|
|
|
|
|
|
|
// We write the highest index first, to have only one bound check.
|
|
|
|
buffer[13] = (byte)count; // The total number of profiles.
|
|
|
|
buffer[12] = (byte)current; // The position within the collection.
|
|
|
|
buffer[11] = 0x00; |
|
|
|
buffer[0] = (byte)'I'; |
|
|
|
buffer[1] = (byte)'C'; |
|
|
|
buffer[2] = (byte)'C'; |
|
|
|
buffer[3] = (byte)'_'; |
|
|
|
buffer[4] = (byte)'P'; |
|
|
|
buffer[5] = (byte)'R'; |
|
|
|
buffer[6] = (byte)'O'; |
|
|
|
buffer[7] = (byte)'F'; |
|
|
|
buffer[8] = (byte)'I'; |
|
|
|
buffer[9] = (byte)'L'; |
|
|
|
buffer[10] = (byte)'E'; |
|
|
|
|
|
|
|
this.outputStream.Write(buffer, 0, iccOverheadLength); |
|
|
|
this.outputStream.Write(data, offset, length); |
|
|
|
|
|
|
|
current++; |
|
|
|
@ -481,7 +487,8 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// Writes the metadata profiles to the image.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="metadata">The image metadata.</param>
|
|
|
|
private void WriteProfiles(ImageMetadata metadata) |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteProfiles(ImageMetadata metadata, Span<byte> buffer) |
|
|
|
{ |
|
|
|
if (metadata is null) |
|
|
|
{ |
|
|
|
@ -494,10 +501,10 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
// - APP2 ICC
|
|
|
|
// - APP13 IPTC
|
|
|
|
metadata.SyncProfiles(); |
|
|
|
this.WriteExifProfile(metadata.ExifProfile); |
|
|
|
this.WriteXmpProfile(metadata.XmpProfile); |
|
|
|
this.WriteIccProfile(metadata.IccProfile); |
|
|
|
this.WriteIptcProfile(metadata.IptcProfile); |
|
|
|
this.WriteExifProfile(metadata.ExifProfile, buffer); |
|
|
|
this.WriteXmpProfile(metadata.XmpProfile, buffer); |
|
|
|
this.WriteIccProfile(metadata.IccProfile, buffer); |
|
|
|
this.WriteIptcProfile(metadata.IptcProfile, buffer); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -506,25 +513,26 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// <param name="width">The frame width.</param>
|
|
|
|
/// <param name="height">The frame height.</param>
|
|
|
|
/// <param name="frame">The frame configuration.</param>
|
|
|
|
private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame) |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame, Span<byte> buffer) |
|
|
|
{ |
|
|
|
JpegComponentConfig[] components = frame.Components; |
|
|
|
|
|
|
|
// Length (high byte, low byte), 8 + components * 3.
|
|
|
|
int markerlen = 8 + (3 * components.Length); |
|
|
|
this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen); |
|
|
|
this.buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
|
|
|
|
this.buffer[1] = (byte)(height >> 8); |
|
|
|
this.buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
|
|
|
|
this.buffer[3] = (byte)(width >> 8); |
|
|
|
this.buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
|
|
|
|
this.buffer[5] = (byte)components.Length; |
|
|
|
this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer); |
|
|
|
buffer[5] = (byte)components.Length; |
|
|
|
buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
|
|
|
|
buffer[1] = (byte)(height >> 8); |
|
|
|
buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
|
|
|
|
buffer[3] = (byte)(width >> 8); |
|
|
|
buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
|
|
|
|
|
|
|
|
// Components data
|
|
|
|
for (int i = 0; i < components.Length; i++) |
|
|
|
{ |
|
|
|
int i3 = 3 * i; |
|
|
|
Span<byte> bufferSpan = this.buffer.AsSpan(i3 + 6, 3); |
|
|
|
Span<byte> bufferSpan = buffer.Slice(i3 + 6, 3); |
|
|
|
|
|
|
|
// Quantization table selector
|
|
|
|
bufferSpan[2] = (byte)components[i].QuantizatioTableIndex; |
|
|
|
@ -538,14 +546,15 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
bufferSpan[0] = components[i].Id; |
|
|
|
} |
|
|
|
|
|
|
|
this.outputStream.Write(this.buffer, 0, (3 * (components.Length - 1)) + 9); |
|
|
|
this.outputStream.Write(buffer, 0, (3 * (components.Length - 1)) + 9); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Writes the StartOfScan marker.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="components">The collecction of component configuration items.</param>
|
|
|
|
private void WriteStartOfScan(Span<JpegComponentConfig> components) |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer) |
|
|
|
{ |
|
|
|
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
|
|
|
|
// - the marker length "\x00\x0c",
|
|
|
|
@ -556,14 +565,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
// - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
|
|
|
|
// sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
|
|
|
|
// should be 0x00, 0x3f, 0x00<<4 | 0x00.
|
|
|
|
this.buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
this.buffer[1] = JpegConstants.Markers.SOS; |
|
|
|
buffer[1] = JpegConstants.Markers.SOS; |
|
|
|
buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
|
|
|
|
// Length (high byte, low byte), must be 6 + 2 * (number of components in scan)
|
|
|
|
int sosSize = 6 + (2 * components.Length); |
|
|
|
this.buffer[2] = 0x00; |
|
|
|
this.buffer[3] = (byte)sosSize; |
|
|
|
this.buffer[4] = (byte)components.Length; // Number of components in a scan
|
|
|
|
buffer[4] = (byte)components.Length; // Number of components in a scan
|
|
|
|
buffer[3] = (byte)sosSize; |
|
|
|
buffer[2] = 0x00; |
|
|
|
|
|
|
|
// Components data
|
|
|
|
for (int i = 0; i < components.Length; i++) |
|
|
|
@ -571,27 +580,28 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
int i2 = 2 * i; |
|
|
|
|
|
|
|
// Id
|
|
|
|
this.buffer[i2 + 5] = components[i].Id; |
|
|
|
buffer[i2 + 5] = components[i].Id; |
|
|
|
|
|
|
|
// Table selectors
|
|
|
|
int tableSelectors = (components[i].DcTableSelector << 4) | components[i].AcTableSelector; |
|
|
|
this.buffer[i2 + 6] = (byte)tableSelectors; |
|
|
|
buffer[i2 + 6] = (byte)tableSelectors; |
|
|
|
} |
|
|
|
|
|
|
|
this.buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
|
|
|
|
this.buffer[sosSize] = 0x3f; // Se - End of spectral selection.
|
|
|
|
this.buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
|
|
|
|
this.outputStream.Write(this.buffer, 0, sosSize + 2); |
|
|
|
buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
|
|
|
|
buffer[sosSize] = 0x3f; // Se - End of spectral selection.
|
|
|
|
buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
|
|
|
|
this.outputStream.Write(buffer, 0, sosSize + 2); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Writes the EndOfImage marker.
|
|
|
|
/// </summary>
|
|
|
|
private void WriteEndOfImageMarker() |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteEndOfImageMarker(Span<byte> buffer) |
|
|
|
{ |
|
|
|
this.buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
this.buffer[1] = JpegConstants.Markers.EOI; |
|
|
|
this.outputStream.Write(this.buffer, 0, 2); |
|
|
|
buffer[1] = JpegConstants.Markers.EOI; |
|
|
|
buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
this.outputStream.Write(buffer, 0, 2); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -602,12 +612,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// <param name="frameConfig">The frame configuration.</param>
|
|
|
|
/// <param name="spectralConverter">The spectral converter.</param>
|
|
|
|
/// <param name="encoder">The scan encoder.</param>
|
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
|
|
private void WriteHuffmanScans<TPixel>( |
|
|
|
JpegFrame frame, |
|
|
|
JpegFrameConfig frameConfig, |
|
|
|
SpectralConverter<TPixel> spectralConverter, |
|
|
|
HuffmanScanEncoder encoder, |
|
|
|
Span<byte> buffer, |
|
|
|
CancellationToken cancellationToken) |
|
|
|
where TPixel : unmanaged, IPixel<TPixel> |
|
|
|
{ |
|
|
|
@ -615,14 +627,14 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
{ |
|
|
|
frame.AllocateComponents(fullScan: false); |
|
|
|
|
|
|
|
this.WriteStartOfScan(frameConfig.Components); |
|
|
|
this.WriteStartOfScan(frameConfig.Components, buffer); |
|
|
|
encoder.EncodeScanBaselineSingleComponent(frame.Components[0], spectralConverter, cancellationToken); |
|
|
|
} |
|
|
|
else if (frame.Interleaved) |
|
|
|
{ |
|
|
|
frame.AllocateComponents(fullScan: false); |
|
|
|
|
|
|
|
this.WriteStartOfScan(frameConfig.Components); |
|
|
|
this.WriteStartOfScan(frameConfig.Components, buffer); |
|
|
|
encoder.EncodeScanBaselineInterleaved(frameConfig.EncodingColor, frame, spectralConverter, cancellationToken); |
|
|
|
} |
|
|
|
else |
|
|
|
@ -633,7 +645,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
Span<JpegComponentConfig> components = frameConfig.Components; |
|
|
|
for (int i = 0; i < frame.Components.Length; i++) |
|
|
|
{ |
|
|
|
this.WriteStartOfScan(components.Slice(i, 1)); |
|
|
|
this.WriteStartOfScan(components.Slice(i, 1), buffer); |
|
|
|
encoder.EncodeScanBaseline(frame.Components[i], cancellationToken); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -644,14 +656,16 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// </summary>
|
|
|
|
/// <param name="marker">The marker to write.</param>
|
|
|
|
/// <param name="length">The marker length.</param>
|
|
|
|
private void WriteMarkerHeader(byte marker, int length) |
|
|
|
/// <param name="buffer">Temporary buffer.</param>
|
|
|
|
private void WriteMarkerHeader(byte marker, int length, Span<byte> buffer) |
|
|
|
{ |
|
|
|
// Markers are always prefixed with 0xff.
|
|
|
|
this.buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
this.buffer[1] = marker; |
|
|
|
this.buffer[2] = (byte)(length >> 8); |
|
|
|
this.buffer[3] = (byte)(length & 0xff); |
|
|
|
this.outputStream.Write(this.buffer, 0, 4); |
|
|
|
buffer[3] = (byte)(length & 0xff); |
|
|
|
buffer[2] = (byte)(length >> 8); |
|
|
|
buffer[1] = marker; |
|
|
|
buffer[0] = JpegConstants.Markers.XFF; |
|
|
|
|
|
|
|
this.outputStream.Write(buffer, 0, 4); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -668,15 +682,16 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals |
|
|
|
/// <param name="configs">Quantization tables configs.</param>
|
|
|
|
/// <param name="optionsQuality">Optional quality value from the options.</param>
|
|
|
|
/// <param name="metadata">Jpeg metadata instance.</param>
|
|
|
|
private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs, int? optionsQuality, JpegMetadata metadata) |
|
|
|
/// <param name="tmpBuffer">Temporary buffer.</param>
|
|
|
|
private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs, int? optionsQuality, JpegMetadata metadata, Span<byte> tmpBuffer) |
|
|
|
{ |
|
|
|
int dataLen = configs.Length * (1 + Block8x8.Size); |
|
|
|
|
|
|
|
// Marker + quantization table lengths.
|
|
|
|
int markerlen = 2 + dataLen; |
|
|
|
this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen); |
|
|
|
this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen, tmpBuffer); |
|
|
|
|
|
|
|
byte[] buffer = new byte[dataLen]; |
|
|
|
Span<byte> buffer = dataLen <= 256 ? stackalloc byte[dataLen] : new byte[dataLen]; |
|
|
|
int offset = 0; |
|
|
|
|
|
|
|
Block8x8F workspaceBlock = default; |
|
|
|
|