@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// The only supported precision
/// </summary>
private readonly int [ ] supportedPrecisions = { 8 , 1 2 } ;
private readonly byte [ ] supportedPrecisions = { 8 , 1 2 } ;
/// <summary>
/// The buffer used to temporarily store bytes read from the stream.
@ -42,21 +42,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private readonly byte [ ] markerBuffer = new byte [ 2 ] ;
/// <summary>
/// The DC Huffman tables.
/// </summary>
private HuffmanTable [ ] dcHuffmanTables ;
/// <summary>
/// The AC Huffman tables
/// </summary>
private HuffmanTable [ ] acHuffmanTables ;
/// <summary>
/// The reset interval determined by RST markers.
/// </summary>
private ushort resetInterval ;
/// <summary>
/// Whether the image has an EXIF marker.
/// </summary>
@ -97,6 +82,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private AdobeMarker adobe ;
/// <summary>
/// Scan decoder.
/// </summary>
private HuffmanScanDecoder scanDecoder ;
/// <summary>
/// Initializes a new instance of the <see cref="JpegDecoderCore" /> class.
/// </summary>
@ -117,30 +107,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public JpegFrame Frame { get ; private set ; }
/// <inheritdoc/>
public Size ImageSizeInPixels { get ; private set ; }
/// <inheritdoc/>
Size IImageDecoderInternals . Dimensions = > this . ImageSizeInPixels ;
/// <summary>
/// Gets the number of MCU blocks in the image as <see cref="Size"/>.
/// </summary>
public Size ImageSizeInMCU { get ; private set ; }
/// <summary>
/// Gets the image width
/// </summary>
public int ImageWidth = > this . ImageSizeInPixels . Width ;
/// <summary>
/// Gets the image height
/// </summary>
public int ImageHeight = > this . ImageSizeInPixels . Height ;
/// <summary>
/// Gets the color depth, in number of bits per pixel.
/// </summary>
public int BitsPerPixel = > this . ComponentCount * this . Frame . Precision ;
Size IImageDecoderInternals . Dimensions = > this . Frame . PixelSize ;
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
@ -152,15 +119,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
public ImageMetadata Metadata { get ; private set ; }
/// <inheritdoc/>
public int ComponentCount { get ; private set ; }
/// <inheritdoc/>
public JpegColorSpace ColorSpace { get ; private set ; }
/// <inheritdoc/>
public int Precision { get ; private set ; }
/// <summary>
/// Gets the components.
/// </summary>
@ -213,34 +174,44 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public Image < TPixel > Decode < TPixel > ( BufferedReadStream stream , CancellationToken cancellationToken )
where TPixel : unmanaged , IPixel < TPixel >
{
this . ParseStream ( stream , cancellationToken : cancellationToken ) ;
using var spectralConverter = new SpectralConverter < TPixel > ( this . Configuration , cancellationToken ) ;
var scanDecoder = new HuffmanScanDecoder ( stream , spectralConverter , cancellationToken ) ;
this . ParseStream ( stream , scanDecoder , cancellationToken ) ;
this . InitExifProfile ( ) ;
this . InitIccProfile ( ) ;
this . InitIptcProfile ( ) ;
this . InitDerivedMetadataProperties ( ) ;
return this . PostProcessIntoImage < TPixel > ( cancellationToken ) ;
return new Image < TPixel > ( this . Configuration , spectralConverter . PixelBuffer , this . Metadata ) ;
}
/// <inheritdoc/>
public IImageInfo Identify ( BufferedReadStream stream , CancellationToken cancellationToken )
{
this . ParseStream ( stream , true , cancellationToken ) ;
this . ParseStream ( stream , scanDecoder : null , cancellationToken ) ;
this . InitExifProfile ( ) ;
this . InitIccProfile ( ) ;
this . InitIptcProfile ( ) ;
this . InitDerivedMetadataProperties ( ) ;
return new ImageInfo ( new PixelTypeInfo ( this . BitsPerPixel ) , this . ImageWidth , this . ImageHeight , this . Metadata ) ;
Size pixelSize = this . Frame . PixelSize ;
return new ImageInfo ( new PixelTypeInfo ( this . Frame . BitsPerPixel ) , pixelSize . Width , pixelSize . Height , this . Metadata ) ;
}
/// <summary>
/// Parses the input stream for file markers
/// Parses the input stream for file markers.
/// </summary>
/// <param name="stream">The input stream</param>
/// <param name="metadataOnly">Whether to decode metadata only .</param>
/// <param name="stream">The input stream. </param>
/// <param name="scanDecoder">Scan decoder used exclusively to decode SOS marker .</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
public void ParseStream ( BufferedReadStream stream , bool metadataOnly = false , CancellationToken cancellationToken = default )
internal void ParseStream ( BufferedReadStream stream , HuffmanScanDecoder scanDecoder , CancellationToken cancellationToken )
{
bool metadataOnly = scanDecoder = = null ;
this . scanDecoder = scanDecoder ;
this . Metadata = new ImageMetadata ( ) ;
// Check for the Start Of Image marker.
@ -256,14 +227,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
fileMarker = new JpegFileMarker ( marker , ( int ) stream . Position - 2 ) ;
this . QuantizationTables = new Block8x8F [ 4 ] ;
// Only assign what we need
if ( ! metadataOnly )
{
const int maxTables = 4 ;
this . dcHuffmanTables = new HuffmanTable [ maxTables ] ;
this . acHuffmanTables = new HuffmanTable [ maxTables ] ;
}
// Break only when we discover a valid EOI marker.
// https://github.com/SixLabors/ImageSharp/issues/695
while ( fileMarker . Marker ! = JpegConstants . Markers . EOI
@ -287,7 +250,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants . Markers . SOS :
if ( ! metadataOnly )
{
this . ProcessStartOfScanMarker ( stream , cancellationToken ) ;
this . ProcessStartOfScanMarker ( stream , remaining , cancellationToken ) ;
break ;
}
else
@ -378,22 +341,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Set large fields to null.
this . Frame = null ;
this . dcHuffmanTables = null ;
this . acHuffmanTables = null ;
this . scanDecoder = null ;
}
/// <summary>
/// Returns the correct colorspace based on the image component count
/// </summary>
/// <returns>The <see cref="JpegColorSpace"/></returns>
private JpegColorSpace DeduceJpegColorSpace ( )
private JpegColorSpace DeduceJpegColorSpace ( byte componentCount )
{
if ( this . C omponentCount = = 1 )
if ( c omponentCount = = 1 )
{
return JpegColorSpace . Grayscale ;
}
if ( this . C omponentCount = = 3 )
if ( c omponentCount = = 3 )
{
if ( ! this . adobe . Equals ( default ) & & this . adobe . ColorTransform = = JpegConstants . Adobe . ColorTransformUnknown )
{
@ -405,14 +367,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return JpegColorSpace . YCbCr ;
}
if ( this . C omponentCount = = 4 )
if ( c omponentCount = = 4 )
{
return this . adobe . ColorTransform = = JpegConstants . Adobe . ColorTransformYcck
? JpegColorSpace . Ycck
: JpegColorSpace . Cmyk ;
}
JpegThrowHelper . ThrowInvalidImageContentException ( $"Unsupported color mode. Supported component counts 1, 3, and 4; found {this.C omponentCount}" ) ;
JpegThrowHelper . ThrowInvalidImageContentException ( $"Unsupported color mode. Supported component counts 1, 3, and 4; found {c omponentCount}" ) ;
return default ;
}
@ -551,7 +513,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper . ThrowInvalidImageContentException ( "Bad App1 Marker length." ) ;
}
var profile = new byte [ remaining ] ;
byte [ ] profile = new byte [ remaining ] ;
stream . Read ( profile , 0 , remaining ) ;
if ( ProfileResolver . IsProfile ( profile , ProfileResolver . ExifMarker ) )
@ -585,14 +547,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return ;
}
var identifier = new byte [ Icclength ] ;
byte [ ] identifier = new byte [ Icclength ] ;
stream . Read ( identifier , 0 , Icclength ) ;
remaining - = Icclength ; // We have read it by this point
if ( ProfileResolver . IsProfile ( identifier , ProfileResolver . IccMarker ) )
{
this . isIcc = true ;
var profile = new byte [ remaining ] ;
byte [ ] profile = new byte [ remaining ] ;
stream . Read ( profile , 0 , remaining ) ;
if ( this . iccData is null )
@ -630,7 +592,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
remaining - = ProfileResolver . AdobePhotoshopApp13Marker . Length ;
if ( ProfileResolver . IsProfile ( this . temp , ProfileResolver . AdobePhotoshopApp13Marker ) )
{
var resourceBlockData = new byte [ remaining ] ;
byte [ ] resourceBlockData = new byte [ remaining ] ;
stream . Read ( resourceBlockData , 0 , remaining ) ;
Span < byte > blockDataSpan = resourceBlockData . AsSpan ( ) ;
@ -645,8 +607,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
Span < byte > imageResourceBlockId = blockDataSpan . Slice ( 0 , 2 ) ;
if ( ProfileResolver . IsProfile ( imageResourceBlockId , ProfileResolver . AdobeIptcMarker ) )
{
var resourceBlockNameLength = ReadImageResourceNameLength ( blockDataSpan ) ;
var resourceDataSize = ReadResourceDataLength ( blockDataSpan , resourceBlockNameLength ) ;
int resourceBlockNameLength = ReadImageResourceNameLength ( blockDataSpan ) ;
int resourceDataSize = ReadResourceDataLength ( blockDataSpan , resourceBlockNameLength ) ;
int dataStartIdx = 2 + resourceBlockNameLength + 4 ;
if ( resourceDataSize > 0 & & blockDataSpan . Length > = dataStartIdx + resourceDataSize )
{
@ -657,8 +619,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
else
{
var resourceBlockNameLength = ReadImageResourceNameLength ( blockDataSpan ) ;
var resourceDataSize = ReadResourceDataLength ( blockDataSpan , resourceBlockNameLength ) ;
int resourceBlockNameLength = ReadImageResourceNameLength ( blockDataSpan ) ;
int resourceDataSize = ReadResourceDataLength ( blockDataSpan , resourceBlockNameLength ) ;
int dataStartIdx = 2 + resourceBlockNameLength + 4 ;
if ( blockDataSpan . Length < dataStartIdx + resourceDataSize )
{
@ -681,7 +643,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private static int ReadImageResourceNameLength ( Span < byte > blockDataSpan )
{
byte nameLength = blockDataSpan [ 2 ] ;
var nameDataSize = nameLength = = 0 ? 2 : nameLength ;
int nameDataSize = nameLength = = 0 ? 2 : nameLength ;
if ( nameDataSize % 2 ! = 0 )
{
nameDataSize + + ;
@ -698,9 +660,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <returns>The block length.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int ReadResourceDataLength ( Span < byte > blockDataSpan , int resourceBlockNameLength )
{
return BinaryPrimitives . ReadInt32BigEndian ( blockDataSpan . Slice ( 2 + resourceBlockNameLength , 4 ) ) ;
}
= > BinaryPrimitives . ReadInt32BigEndian ( blockDataSpan . Slice ( 2 + resourceBlockNameLength , 4 ) ) ;
/// <summary>
/// Processes the application header containing the Adobe identifier
@ -835,58 +795,62 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper . ThrowInvalidImageContentException ( "Multiple SOF markers. Only single frame jpegs supported." ) ;
}
// Read initial marker definitions.
// Read initial marker definitions
const int length = 6 ;
stream . Read ( this . temp , 0 , length ) ;
// We only support 8-bit and 12-bit precision.
if ( Array . IndexOf ( this . supportedPrecisions , this . temp [ 0 ] ) = = - 1 )
// 1 byte: Bits/sample precision
byte precision = this . temp [ 0 ] ;
// Validate: only 8-bit and 12-bit precisions are supported
if ( Array . IndexOf ( this . supportedPrecisions , precision ) = = - 1 )
{
JpegThrowHelper . ThrowInvalidImageContentException ( "Only 8-Bit and 12-Bit precision supported." ) ;
}
this . Precision = this . temp [ 0 ] ;
// 2 byte: Height
int frameHeight = ( this . temp [ 1 ] < < 8 ) | this . temp [ 2 ] ;
this . Frame = new JpegFrame
{
Extended = frameMarker . Marker = = JpegConstants . Markers . SOF1 ,
Progressive = frameMarker . Marker = = JpegConstants . Markers . SOF2 ,
Precision = this . temp [ 0 ] ,
Scanlines = ( this . temp [ 1 ] < < 8 ) | this . temp [ 2 ] ,
SamplesPerLine = ( this . temp [ 3 ] < < 8 ) | this . temp [ 4 ] ,
ComponentCount = this . temp [ 5 ]
} ;
if ( this . Frame . SamplesPerLine = = 0 | | this . Frame . Scanlines = = 0 )
// 2 byte: Width
int frameWidth = ( this . temp [ 3 ] < < 8 ) | this . temp [ 4 ] ;
// Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that)
if ( frameHeight = = 0 | | frameWidth = = 0 )
{
JpegThrowHelper . ThrowInvalidImageDimensions ( this . Frame . SamplesPerLine , this . Frame . Scanlines ) ;
JpegThrowHelper . ThrowInvalidImageDimensions ( frameWidth , frameHeight ) ;
}
this . ImageSizeInPixels = new Size ( this . Frame . SamplesPerLine , this . Frame . Scanlines ) ;
this . ComponentCount = this . Frame . ComponentCount ;
// 1 byte: Number of components
byte componentCount = this . temp [ 5 ] ;
this . ColorSpace = this . DeduceJpegColorSpace ( componentCount ) ;
this . Metadata . GetJpegMetadata ( ) . ColorType = this . ColorSpace = = JpegColorSpace . Grayscale ? JpegColorType . Luminance : JpegColorType . YCbCr ;
this . Frame = new JpegFrame ( frameMarker , precision , frameWidth , frameHeight , componentCount ) ;
if ( ! metadataOnly )
{
remaining - = length ;
// Validate: remaining part must be equal to components * 3
const int componentBytes = 3 ;
if ( remaining > this . C omponentCount * componentBytes )
if ( remaining ! = c omponentCount * componentBytes )
{
JpegThrowHelper . ThrowBadMarker ( "SOFn" , remaining ) ;
}
// components*3 bytes: component data
stream . Read ( this . temp , 0 , remaining ) ;
// No need to pool this. They max out at 4
this . Frame . ComponentIds = new byte [ this . ComponentCount ] ;
this . Frame . ComponentOrder = new byte [ this . ComponentCount ] ;
this . Frame . Components = new JpegComponent [ this . ComponentCount ] ;
this . ColorSpace = this . DeduceJpegColorSpace ( ) ;
this . Frame . ComponentIds = new byte [ componentCount ] ;
this . Frame . ComponentOrder = new byte [ componentCount ] ;
this . Frame . Components = new JpegComponent [ componentCount ] ;
int maxH = 0 ;
int maxV = 0 ;
int index = 0 ;
for ( int i = 0 ; i < this . C omponentCount; i + + )
for ( int i = 0 ; i < c omponentCount; i + + )
{
byte hv = this . temp [ index + 1 ] ;
int h = ( hv > > 4 ) & 1 5 ;
@ -910,12 +874,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
index + = componentBytes ;
}
this . Frame . MaxHorizontalFactor = maxH ;
this . Frame . MaxVerticalFactor = maxV ;
this . ColorSpace = this . DeduceJpegColorSpace ( ) ;
this . Metadata . GetJpegMetadata ( ) . ColorType = this . ColorSpace = = JpegColorSpace . Grayscale ? JpegColorType . Luminance : JpegColorType . YCbCr ;
this . Frame . InitComponents ( ) ;
this . ImageSizeInMCU = new Size ( this . Frame . McusPerLine , this . Frame . McusPerColumn ) ;
this . Frame . Init ( maxH , maxV ) ;
this . scanDecoder . InjectFrameData ( this . Frame , this ) ;
}
}
@ -978,8 +939,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
i + = 1 7 + codeLengthSum ;
this . BuildHuffmanTable (
tableType = = 0 ? this . dcHuffmanTables : this . acHuffmanTables ,
this . scanDecoder . BuildHuffmanTable (
tableType ,
tableIndex ,
codeLengthsSpan ,
huffmanValuesSpan ) ;
@ -1002,80 +963,101 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper . ThrowBadMarker ( nameof ( JpegConstants . Markers . DRI ) , remaining ) ;
}
this . resetInterval = this . ReadUint16 ( stream ) ;
this . scanDecode r. R esetInterval = this . ReadUint16 ( stream ) ;
}
/// <summary>
/// Processes the SOS (Start of scan marker).
/// </summary>
private void ProcessStartOfScanMarker ( BufferedReadStream stream , CancellationToken cancellationToken )
private void ProcessStartOfScanMarker ( BufferedReadStream stream , int remaining , CancellationToken cancellationToken )
{
if ( this . Frame is null )
{
JpegThrowHelper . ThrowInvalidImageContentException ( "No readable SOFn (Start Of Frame) marker found." ) ;
}
// 1 byte: Number of components in scan
int selectorsCount = stream . ReadByte ( ) ;
for ( int i = 0 ; i < selectorsCount ; i + + )
// Validate: 0 < count <= totalComponents
if ( selectorsCount = = 0 | | selectorsCount > this . Frame . ComponentCount )
{
int componentIndex = - 1 ;
int selector = stream . ReadByte ( ) ;
// TODO: extract as separate method?
JpegThrowHelper . ThrowInvalidImageContentException ( $"Invalid number of components in scan: {selectorsCount}." ) ;
}
// Validate: marker must contain exactly (4 + selectorsCount*2) bytes
int selectorsBytes = selectorsCount * 2 ;
if ( remaining ! = 4 + selectorsBytes )
{
JpegThrowHelper . ThrowBadMarker ( "SOS" , remaining ) ;
}
// selectorsCount*2 bytes: component index + huffman tables indices
stream . Read ( this . temp , 0 , selectorsBytes ) ;
this . Frame . MultiScan = this . Frame . ComponentCount ! = selectorsCount ;
for ( int i = 0 ; i < selectorsBytes ; i + = 2 )
{
// 1 byte: Component id
int componentSelectorId = this . temp [ i ] ;
int componentIndex = - 1 ;
for ( int j = 0 ; j < this . Frame . ComponentIds . Length ; j + + )
{
byte id = this . Frame . ComponentIds [ j ] ;
if ( selector = = id )
if ( componentSelectorId = = id )
{
componentIndex = j ;
break ;
}
}
if ( componentIndex < 0 )
// Validate: must be found among registered components
if ( componentIndex = = - 1 )
{
// TODO: extract as separate method?
JpegThrowHelper . ThrowInvalidImageContentException ( $"Unknown component id in scan: {componentSelectorId}." ) ;
}
this . Frame . ComponentOrder [ i / 2 ] = ( byte ) componentIndex ;
JpegComponent component = this . Frame . Components [ componentIndex ] ;
// 1 byte: Huffman table selectors.
// 4 bits - dc
// 4 bits - ac
int tableSpec = this . temp [ i + 1 ] ;
int dcTableIndex = tableSpec > > 4 ;
int acTableIndex = tableSpec & 1 5 ;
// Validate: both must be < 4
if ( dcTableIndex > = 4 | | acTableIndex > = 4 )
{
JpegThrowHelper . ThrowInvalidImageContentException ( $"Unknown component selector {componentIndex}." ) ;
// TODO: extract as separate method?
JpegThrowHelper . ThrowInvalidImageContentException ( $"Invalid huffman table for component:{componentSelectorId}: dc={dcTableIndex}, ac={acTableIndex}" ) ;
}
ref JpegComponent component = ref this . Frame . Components [ componentIndex ] ;
int tableSpec = stream . ReadByte ( ) ;
component . DCHuffmanTableId = tableSpec > > 4 ;
component . ACHuffmanTableId = tableSpec & 1 5 ;
this . Frame . ComponentOrder [ i ] = ( byte ) componentIndex ;
component . DCHuffmanTableId = dcTableIndex ;
component . ACHuffmanTableId = acTableIndex ;
}
// 3 bytes: Progressive scan decoding data
stream . Read ( this . temp , 0 , 3 ) ;
int spectralStart = this . temp [ 0 ] ;
this . scanDecoder . SpectralStart = spectralStart ;
int spectralEnd = this . temp [ 1 ] ;
this . scanDecoder . SpectralEnd = spectralEnd ;
int successiveApproximation = this . temp [ 2 ] ;
this . scanDecoder . SuccessiveHigh = successiveApproximation > > 4 ;
this . scanDecoder . SuccessiveLow = successiveApproximation & 1 5 ;
var sd = new HuffmanScanDecoder (
stream ,
this . Frame ,
this . dcHuffmanTables ,
this . acHuffmanTables ,
selectorsCount ,
this . resetInterval ,
spectralStart ,
spectralEnd ,
successiveApproximation > > 4 ,
successiveApproximation & 1 5 ,
cancellationToken ) ;
sd . ParseEntropyCodedData ( ) ;
this . scanDecoder . ParseEntropyCodedData ( selectorsCount ) ;
}
/// <summary>
/// Builds the huffman tables
/// </summary>
/// <param name="tables">The tables</param>
/// <param name="index">The table index</param>
/// <param name="codeLengths">The codelengths</param>
/// <param name="values">The values</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void BuildHuffmanTable ( HuffmanTable [ ] tables , int index , ReadOnlySpan < byte > codeLengths , ReadOnlySpan < byte > values )
= > tables [ index ] = new HuffmanTable ( codeLengths , values ) ;
/// <summary>
/// Reads a <see cref="ushort"/> from the stream advancing it by two bytes
/// </summary>
@ -1087,32 +1069,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
stream . Read ( this . markerBuffer , 0 , 2 ) ;
return BinaryPrimitives . ReadUInt16BigEndian ( this . markerBuffer ) ;
}
/// <summary>
/// Post processes the pixels into the destination image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
private Image < TPixel > PostProcessIntoImage < TPixel > ( CancellationToken cancellationToken )
where TPixel : unmanaged , IPixel < TPixel >
{
if ( this . ImageWidth = = 0 | | this . ImageHeight = = 0 )
{
JpegThrowHelper . ThrowInvalidImageDimensions ( this . ImageWidth , this . ImageHeight ) ;
}
var image = Image . CreateUninitialized < TPixel > (
this . Configuration ,
this . ImageWidth ,
this . ImageHeight ,
this . Metadata ) ;
using ( var postProcessor = new JpegImagePostProcessor ( this . Configuration , this ) )
{
postProcessor . PostProcess ( image . Frames . RootFrame , cancellationToken ) ;
}
return image ;
}
}
}