Skip to content

Commit 6ec1692

Browse files
Merge pull request #2363 from IldarKhayrutdinov/tiff-frames-meta
Support frames metadata for Identify
2 parents 215c609 + 24a0a5f commit 6ec1692

34 files changed

+672
-200
lines changed

.github/workflows/build-and-test.yml

+6-2
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,14 @@ jobs:
198198

199199
- name: Feedz Publish
200200
shell: pwsh
201-
run: dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/nuget/index.json -ss https://f.feedz.io/sixlabors/sixlabors/symbols --skip-duplicate
201+
run: |
202+
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/nuget/index.json --skip-duplicate
203+
dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/symbols --skip-duplicate
202204
203205
- name: NuGet Publish
204206
if: ${{ startsWith(github.ref, 'refs/tags/') }}
205207
shell: pwsh
206-
run: dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
208+
run: |
209+
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
210+
dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
207211

src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
208208
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
209209
{
210210
this.ReadImageHeaders(stream, out _, out _);
211-
return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metadata);
211+
return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), new(this.infoHeader.Width, this.infoHeader.Height), this.metadata);
212212
}
213213

214214
/// <summary>

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

+57-15
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
172172
/// <inheritdoc />
173173
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
174174
{
175+
uint frameCount = 0;
176+
ImageFrameMetadata? previousFrame = null;
177+
List<ImageFrameMetadata> framesMetadata = new();
175178
try
176179
{
177180
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
@@ -182,14 +185,23 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
182185
{
183186
if (nextFlag == GifConstants.ImageLabel)
184187
{
185-
this.ReadImageDescriptor(stream);
188+
if (previousFrame != null && ++frameCount == this.maxFrames)
189+
{
190+
break;
191+
}
192+
193+
this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame);
194+
195+
// Reset per-frame state.
196+
this.imageDescriptor = default;
197+
this.graphicsControlExtension = default;
186198
}
187199
else if (nextFlag == GifConstants.ExtensionIntroducer)
188200
{
189201
switch (stream.ReadByte())
190202
{
191203
case GifConstants.GraphicControlLabel:
192-
SkipBlock(stream); // Skip graphic control extension block
204+
this.ReadGraphicalControlExtension(stream);
193205
break;
194206
case GifConstants.CommentLabel:
195207
this.ReadComments(stream);
@@ -226,9 +238,9 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
226238

227239
return new ImageInfo(
228240
new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel),
229-
this.logicalScreenDescriptor.Width,
230-
this.logicalScreenDescriptor.Height,
231-
this.metadata);
241+
new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
242+
this.metadata,
243+
framesMetadata);
232244
}
233245

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

489-
this.SetFrameMetadata(image.Frames.RootFrame.Metadata, true);
501+
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
490502

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

500512
currentFrame = image!.Frames.CreateFrame();
501513

502-
this.SetFrameMetadata(currentFrame.Metadata, false);
514+
this.SetFrameMetadata(currentFrame.Metadata);
503515

504516
imageFrame = currentFrame;
505517

@@ -606,6 +618,37 @@ private void ReadFrameColors<TPixel>(ref Image<TPixel>? image, ref ImageFrame<TP
606618
}
607619
}
608620

621+
/// <summary>
622+
/// Reads the frames metadata.
623+
/// </summary>
624+
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
625+
/// <param name="frameMetadata">The collection of frame metadata.</param>
626+
/// <param name="previousFrame">The previous frame metadata.</param>
627+
private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadata> frameMetadata, ref ImageFrameMetadata? previousFrame)
628+
{
629+
this.ReadImageDescriptor(stream);
630+
631+
// Skip the color table for this frame if local.
632+
if (this.imageDescriptor.LocalColorTableFlag)
633+
{
634+
stream.Skip(this.imageDescriptor.LocalColorTableSize * 3);
635+
}
636+
637+
// Skip the frame indices. Pixels length + mincode size.
638+
// The gif format does not tell us the length of the compressed data beforehand.
639+
int minCodeSize = stream.ReadByte();
640+
using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream);
641+
lzwDecoder.SkipIndices(minCodeSize, this.imageDescriptor.Width * this.imageDescriptor.Height);
642+
643+
ImageFrameMetadata currentFrame = new();
644+
frameMetadata.Add(currentFrame);
645+
this.SetFrameMetadata(currentFrame);
646+
previousFrame = currentFrame;
647+
648+
// Skip any remaining blocks
649+
SkipBlock(stream);
650+
}
651+
609652
/// <summary>
610653
/// Restores the current frame area to the background.
611654
/// </summary>
@@ -627,34 +670,33 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
627670
}
628671

629672
/// <summary>
630-
/// Sets the frames metadata.
673+
/// Sets the metadata for the image frame.
631674
/// </summary>
632-
/// <param name="meta">The metadata.</param>
633-
/// <param name="isRoot">Whether the metadata represents the root frame.</param>
675+
/// <param name="metadata">The metadata.</param>
634676
[MethodImpl(MethodImplOptions.AggressiveInlining)]
635-
private void SetFrameMetadata(ImageFrameMetadata meta, bool isRoot)
677+
private void SetFrameMetadata(ImageFrameMetadata metadata)
636678
{
637679
// Frames can either use the global table or their own local table.
638-
if (isRoot && this.logicalScreenDescriptor.GlobalColorTableFlag
680+
if (this.logicalScreenDescriptor.GlobalColorTableFlag
639681
&& this.logicalScreenDescriptor.GlobalColorTableSize > 0)
640682
{
641-
GifFrameMetadata gifMeta = meta.GetGifMetadata();
683+
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
642684
gifMeta.ColorTableMode = GifColorTableMode.Global;
643685
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
644686
}
645687

646688
if (this.imageDescriptor.LocalColorTableFlag
647689
&& this.imageDescriptor.LocalColorTableSize > 0)
648690
{
649-
GifFrameMetadata gifMeta = meta.GetGifMetadata();
691+
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
650692
gifMeta.ColorTableMode = GifColorTableMode.Local;
651693
gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize;
652694
}
653695

654696
// Graphics control extensions is optional.
655697
if (this.graphicsControlExtension != default)
656698
{
657-
GifFrameMetadata gifMeta = meta.GetGifMetadata();
699+
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
658700
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
659701
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
660702
}

src/ImageSharp/Formats/Gif/LzwDecoder.cs

+154-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream)
6161
}
6262

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

235+
/// <summary>
236+
/// Decodes and decompresses all pixel indices from the stream allowing skipping of the data.
237+
/// </summary>
238+
/// <param name="minCodeSize">Minimum code size of the data.</param>
239+
/// <param name="length">The resulting index table length.</param>
240+
public void SkipIndices(int minCodeSize, int length)
241+
{
242+
// Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
243+
int clearCode = 1 << minCodeSize;
244+
245+
// It is possible to specify a larger LZW minimum code size than the palette length in bits
246+
// which may leave a gap in the codes where no colors are assigned.
247+
// http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression
248+
if (minCodeSize < 2 || clearCode > MaxStackSize)
249+
{
250+
// Don't attempt to decode the frame indices.
251+
// Theoretically we could determine a min code size from the length of the provided
252+
// color palette but we won't bother since the image is most likely corrupted.
253+
GifThrowHelper.ThrowInvalidImageContentException("Gif Image does not contain a valid LZW minimum code.");
254+
}
255+
256+
int codeSize = minCodeSize + 1;
257+
258+
// Calculate the end code
259+
int endCode = clearCode + 1;
260+
261+
// Calculate the available code.
262+
int availableCode = clearCode + 2;
263+
264+
// Jillzhangs Code see: http://giflib.codeplex.com/
265+
// Adapted from John Cristy's ImageMagick.
266+
int code;
267+
int oldCode = NullCode;
268+
int codeMask = (1 << codeSize) - 1;
269+
int bits = 0;
270+
271+
int top = 0;
272+
int count = 0;
273+
int bi = 0;
274+
int xyz = 0;
275+
276+
int data = 0;
277+
int first = 0;
278+
279+
ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
280+
ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
281+
ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
282+
283+
for (code = 0; code < clearCode; code++)
284+
{
285+
Unsafe.Add(ref suffixRef, code) = (byte)code;
286+
}
287+
288+
Span<byte> buffer = stackalloc byte[byte.MaxValue];
289+
while (xyz < length)
290+
{
291+
if (top == 0)
292+
{
293+
if (bits < codeSize)
294+
{
295+
// Load bytes until there are enough bits for a code.
296+
if (count == 0)
297+
{
298+
// Read a new data block.
299+
count = this.ReadBlock(buffer);
300+
if (count == 0)
301+
{
302+
break;
303+
}
304+
305+
bi = 0;
306+
}
307+
308+
data += buffer[bi] << bits;
309+
310+
bits += 8;
311+
bi++;
312+
count--;
313+
continue;
314+
}
315+
316+
// Get the next code
317+
code = data & codeMask;
318+
data >>= codeSize;
319+
bits -= codeSize;
320+
321+
// Interpret the code
322+
if (code > availableCode || code == endCode)
323+
{
324+
break;
325+
}
326+
327+
if (code == clearCode)
328+
{
329+
// Reset the decoder
330+
codeSize = minCodeSize + 1;
331+
codeMask = (1 << codeSize) - 1;
332+
availableCode = clearCode + 2;
333+
oldCode = NullCode;
334+
continue;
335+
}
336+
337+
if (oldCode == NullCode)
338+
{
339+
Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
340+
oldCode = code;
341+
first = code;
342+
continue;
343+
}
344+
345+
int inCode = code;
346+
if (code == availableCode)
347+
{
348+
Unsafe.Add(ref pixelStackRef, top++) = (byte)first;
349+
350+
code = oldCode;
351+
}
352+
353+
while (code > clearCode)
354+
{
355+
Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
356+
code = Unsafe.Add(ref prefixRef, code);
357+
}
358+
359+
int suffixCode = Unsafe.Add(ref suffixRef, code);
360+
first = suffixCode;
361+
Unsafe.Add(ref pixelStackRef, top++) = suffixCode;
362+
363+
// Fix for Gifs that have "deferred clear code" as per here :
364+
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
365+
if (availableCode < MaxStackSize)
366+
{
367+
Unsafe.Add(ref prefixRef, availableCode) = oldCode;
368+
Unsafe.Add(ref suffixRef, availableCode) = first;
369+
availableCode++;
370+
if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
371+
{
372+
codeSize++;
373+
codeMask = (1 << codeSize) - 1;
374+
}
375+
}
376+
377+
oldCode = inCode;
378+
}
379+
380+
// Pop a pixel off the pixel stack.
381+
top--;
382+
383+
// Clear missing pixels
384+
xyz++;
385+
}
386+
}
387+
235388
/// <summary>
236389
/// Reads the next data block from the stream. A data block begins with a byte,
237390
/// which defines the size of the block, followed by the block itself.

src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
235235
this.InitDerivedMetadataProperties();
236236

237237
Size pixelSize = this.Frame.PixelSize;
238-
return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata);
238+
return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), new(pixelSize.Width, pixelSize.Height), this.Metadata);
239239
}
240240

241241
/// <summary>

src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
8888

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

9494
/// <summary>

src/ImageSharp/Formats/Png/PngDecoderCore.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
370370
PngThrowHelper.ThrowNoHeader();
371371
}
372372

373-
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata);
373+
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata);
374374
}
375375
finally
376376
{

src/ImageSharp/Formats/Tga/TgaDecoderCore.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -658,8 +658,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
658658
this.ReadFileHeader(stream);
659659
return new ImageInfo(
660660
new PixelTypeInfo(this.fileHeader.PixelDepth),
661-
this.fileHeader.Width,
662-
this.fileHeader.Height,
661+
new(this.fileHeader.Width, this.fileHeader.Height),
663662
this.metadata);
664663
}
665664

0 commit comments

Comments
 (0)