Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support frames metadata for Identify #2363

Merged
merged 20 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1c23ccb
implement adding frames metadata to tiff image metadata
IldarKhayrutdinov Jan 29, 2023
681c921
add TiffInkSet property
IldarKhayrutdinov Feb 20, 2023
561c5d8
assert not supported ink names
IldarKhayrutdinov Feb 20, 2023
de71b0a
universal metadata parsing methods for loading and identify
IldarKhayrutdinov Feb 20, 2023
308bfa4
deep cloning
IldarKhayrutdinov Feb 20, 2023
e1e5f25
Merge remote-tracking branch 'upstream/main' into tiff-frames-meta
IldarKhayrutdinov Feb 20, 2023
220b8aa
add tests
IldarKhayrutdinov Feb 20, 2023
c21bbbd
Allow returning individual image frame metadata via Identify.
JimBobSquarePants Feb 26, 2023
3fcacc6
Merge remote-tracking branch 'upstream/main' into tiff-frames-meta
JimBobSquarePants Feb 26, 2023
468dbcb
Merge branch 'main' into tiff-frames-meta
JimBobSquarePants Feb 26, 2023
bb32439
Merge branch 'main' into tiff-frames-meta
JimBobSquarePants Feb 27, 2023
e9e0e64
update README.md
IldarKhayrutdinov Feb 27, 2023
2ac7c4a
Merge branch 'tiff-frames-meta' of github.com:IldarKhayrutdinov/Image…
IldarKhayrutdinov Feb 27, 2023
d19b128
update README.md
IldarKhayrutdinov Feb 27, 2023
07ca405
add Bit32 value for TiffBitsPerPixel
IldarKhayrutdinov Feb 28, 2023
cfaf555
add test for notSupported 64 bit cmyk, add more tests for Identify (i…
IldarKhayrutdinov Feb 28, 2023
91c3d5e
cleanup
IldarKhayrutdinov Feb 28, 2023
747923d
Add Bit64 and cleanup
JimBobSquarePants Mar 1, 2023
99038f2
Merge remote-tracking branch 'upstream/main' into tiff-frames-meta
JimBobSquarePants Mar 1, 2023
24a0a5f
Update build-and-test.yml
JimBobSquarePants Mar 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ReadImageHeaders(stream, out _, out _);
return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metadata);
return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), new(this.infoHeader.Width, this.infoHeader.Height), this.metadata);
}

/// <summary>
Expand Down
72 changes: 57 additions & 15 deletions src/ImageSharp/Formats/Gif/GifDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
/// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
uint frameCount = 0;
ImageFrameMetadata? previousFrame = null;
List<ImageFrameMetadata> framesMetadata = new();
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
Expand All @@ -182,14 +185,23 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
{
if (nextFlag == GifConstants.ImageLabel)
{
this.ReadImageDescriptor(stream);
if (previousFrame != null && ++frameCount == this.maxFrames)
{
break;
}

this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame);

// Reset per-frame state.
this.imageDescriptor = default;
this.graphicsControlExtension = default;
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
SkipBlock(stream); // Skip graphic control extension block
this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
this.ReadComments(stream);
Expand Down Expand Up @@ -226,9 +238,9 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat

return new ImageInfo(
new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel),
this.logicalScreenDescriptor.Width,
this.logicalScreenDescriptor.Height,
this.metadata);
new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata,
framesMetadata);
}

/// <summary>
Expand Down Expand Up @@ -486,7 +498,7 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel>? image, ref ImageFrame<TP
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
}

this.SetFrameMetadata(image.Frames.RootFrame.Metadata, true);
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);

imageFrame = image.Frames.RootFrame;
}
Expand All @@ -499,7 +511,7 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel>? image, ref ImageFrame<TP

currentFrame = image!.Frames.CreateFrame();

this.SetFrameMetadata(currentFrame.Metadata, false);
this.SetFrameMetadata(currentFrame.Metadata);

imageFrame = currentFrame;

Expand Down Expand Up @@ -606,6 +618,37 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel>? image, ref ImageFrame<TP
}
}

/// <summary>
/// Reads the frames metadata.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="frameMetadata">The collection of frame metadata.</param>
/// <param name="previousFrame">The previous frame metadata.</param>
private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadata> frameMetadata, ref ImageFrameMetadata? previousFrame)
{
this.ReadImageDescriptor(stream);

// Skip the color table for this frame if local.
if (this.imageDescriptor.LocalColorTableFlag)
{
stream.Skip(this.imageDescriptor.LocalColorTableSize * 3);
}

// Skip the frame indices. Pixels length + mincode size.
// The gif format does not tell us the length of the compressed data beforehand.
int minCodeSize = stream.ReadByte();
using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream);
lzwDecoder.SkipIndices(minCodeSize, this.imageDescriptor.Width * this.imageDescriptor.Height);

ImageFrameMetadata currentFrame = new();
frameMetadata.Add(currentFrame);
this.SetFrameMetadata(currentFrame);
previousFrame = currentFrame;

// Skip any remaining blocks
SkipBlock(stream);
}

/// <summary>
/// Restores the current frame area to the background.
/// </summary>
Expand All @@ -627,34 +670,33 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
}

/// <summary>
/// Sets the frames metadata.
/// Sets the metadata for the image frame.
/// </summary>
/// <param name="meta">The metadata.</param>
/// <param name="isRoot">Whether the metadata represents the root frame.</param>
/// <param name="metadata">The metadata.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetFrameMetadata(ImageFrameMetadata meta, bool isRoot)
private void SetFrameMetadata(ImageFrameMetadata metadata)
{
// Frames can either use the global table or their own local table.
if (isRoot && this.logicalScreenDescriptor.GlobalColorTableFlag
if (this.logicalScreenDescriptor.GlobalColorTableFlag
&& this.logicalScreenDescriptor.GlobalColorTableSize > 0)
{
GifFrameMetadata gifMeta = meta.GetGifMetadata();
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global;
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
}

if (this.imageDescriptor.LocalColorTableFlag
&& this.imageDescriptor.LocalColorTableSize > 0)
{
GifFrameMetadata gifMeta = meta.GetGifMetadata();
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local;
gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize;
}

// Graphics control extensions is optional.
if (this.graphicsControlExtension != default)
{
GifFrameMetadata gifMeta = meta.GetGifMetadata();
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
}
Expand Down
155 changes: 154 additions & 1 deletion src/ImageSharp/Formats/Gif/LzwDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream)
}

/// <summary>
/// Decodes and decompresses all pixel indices from the stream.
/// Decodes and decompresses all pixel indices from the stream, assigning the pixel values to the buffer.
/// </summary>
/// <param name="minCodeSize">Minimum code size of the data.</param>
/// <param name="pixels">The pixel array to decode to.</param>
Expand Down Expand Up @@ -232,6 +232,159 @@ public void DecodePixels(int minCodeSize, Buffer2D<byte> pixels)
}
}

/// <summary>
/// Decodes and decompresses all pixel indices from the stream allowing skipping of the data.
/// </summary>
/// <param name="minCodeSize">Minimum code size of the data.</param>
/// <param name="length">The resulting index table length.</param>
public void SkipIndices(int minCodeSize, int length)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gif doesn't tell you in advance the length of the combined LZW segment blocks. You have to read each one which must be decoded to read the next one.

{
// Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
int clearCode = 1 << minCodeSize;

// It is possible to specify a larger LZW minimum code size than the palette length in bits
// which may leave a gap in the codes where no colors are assigned.
// http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression
if (minCodeSize < 2 || clearCode > MaxStackSize)
{
// Don't attempt to decode the frame indices.
// Theoretically we could determine a min code size from the length of the provided
// color palette but we won't bother since the image is most likely corrupted.
GifThrowHelper.ThrowInvalidImageContentException("Gif Image does not contain a valid LZW minimum code.");
}

int codeSize = minCodeSize + 1;

// Calculate the end code
int endCode = clearCode + 1;

// Calculate the available code.
int availableCode = clearCode + 2;

// Jillzhangs Code see: http://giflib.codeplex.com/
// Adapted from John Cristy's ImageMagick.
int code;
int oldCode = NullCode;
int codeMask = (1 << codeSize) - 1;
int bits = 0;

int top = 0;
int count = 0;
int bi = 0;
int xyz = 0;

int data = 0;
int first = 0;

ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());

for (code = 0; code < clearCode; code++)
{
Unsafe.Add(ref suffixRef, code) = (byte)code;
}

Span<byte> buffer = stackalloc byte[byte.MaxValue];
while (xyz < length)
{
if (top == 0)
{
if (bits < codeSize)
{
// Load bytes until there are enough bits for a code.
if (count == 0)
{
// Read a new data block.
count = this.ReadBlock(buffer);
if (count == 0)
{
break;
}

bi = 0;
}

data += buffer[bi] << bits;

bits += 8;
bi++;
count--;
continue;
}

// Get the next code
code = data & codeMask;
data >>= codeSize;
bits -= codeSize;

// Interpret the code
if (code > availableCode || code == endCode)
{
break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This break here is the killer. A lot of properties we require for each loop follow.

}

if (code == clearCode)
{
// Reset the decoder
codeSize = minCodeSize + 1;
codeMask = (1 << codeSize) - 1;
availableCode = clearCode + 2;
oldCode = NullCode;
continue;
}

if (oldCode == NullCode)
{
Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
oldCode = code;
first = code;
continue;
}

int inCode = code;
if (code == availableCode)
{
Unsafe.Add(ref pixelStackRef, top++) = (byte)first;

code = oldCode;
}

while (code > clearCode)
{
Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
code = Unsafe.Add(ref prefixRef, code);
}

int suffixCode = Unsafe.Add(ref suffixRef, code);
first = suffixCode;
Unsafe.Add(ref pixelStackRef, top++) = suffixCode;

// Fix for Gifs that have "deferred clear code" as per here :
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
if (availableCode < MaxStackSize)
{
Unsafe.Add(ref prefixRef, availableCode) = oldCode;
Unsafe.Add(ref suffixRef, availableCode) = first;
availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
{
codeSize++;
codeMask = (1 << codeSize) - 1;
}
}

oldCode = inCode;
}

// Pop a pixel off the pixel stack.
top--;

// Clear missing pixels
xyz++;
}
}

/// <summary>
/// Reads the next data block from the stream. A data block begins with a byte,
/// which defines the size of the block, followed by the block itself.
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
this.InitDerivedMetadataProperties();

Size pixelSize = this.Frame.PixelSize;
return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata);
return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), new(pixelSize.Width, pixelSize.Height), this.Metadata);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat

// BlackAndWhite pixels are encoded into a byte.
int bitsPerPixel = this.componentType == PbmComponentType.Short ? 16 : 8;
return new ImageInfo(new PixelTypeInfo(bitsPerPixel), this.pixelSize.Width, this.pixelSize.Height, this.metadata);
return new ImageInfo(new PixelTypeInfo(bitsPerPixel), new(this.pixelSize.Width, this.pixelSize.Height), this.metadata);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Png/PngDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
PngThrowHelper.ThrowNoHeader();
}

return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata);
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata);
}
finally
{
Expand Down
3 changes: 1 addition & 2 deletions src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -658,8 +658,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
this.ReadFileHeader(stream);
return new ImageInfo(
new PixelTypeInfo(this.fileHeader.PixelDepth),
this.fileHeader.Width,
this.fileHeader.Height,
new(this.fileHeader.Width, this.fileHeader.Height),
this.metadata);
}

Expand Down
26 changes: 26 additions & 0 deletions src/ImageSharp/Formats/Tiff/Constants/TiffInkSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.Metadata.Profiles.Exif;

namespace SixLabors.ImageSharp.Formats.Tiff.Constants;

/// <summary>
/// Enumeration representing the set of inks used in a separated (<see cref="TiffPhotometricInterpretation.Separated"/>) image.
/// </summary>
public enum TiffInkSet : ushort
{
/// <summary>
/// CMYK.
/// The order of the components is cyan, magenta, yellow, black.
/// Usually, a value of 0 represents 0% ink coverage and a value of 255 represents 100% ink coverage for that component, but see DotRange.
/// The <see cref="ExifTagValue.InkNames"/> field should not exist when InkSet=1.
/// </summary>
Cmyk = 1,

/// <summary>
/// Not CMYK.
/// See the <see cref="ExifTagValue.InkNames"/> field for a description of the inks to be used.
/// </summary>
NotCmyk = 2
}
Loading