Skip to content

Commit 4979e99

Browse files
Normalize EXIF XMP and ICC reading.
1 parent 47ac160 commit 4979e99

File tree

3 files changed

+180
-195
lines changed

3 files changed

+180
-195
lines changed

src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ public ImageInfo Identify(
112112
this.webpMetadata = this.metadata.GetWebpMetadata();
113113
this.webpMetadata.RepeatCount = features.AnimationLoopCount;
114114

115-
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
115+
this.webpMetadata.BackgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
116116
? Color.Transparent
117117
: features.AnimationBackgroundColor!.Value;
118118

119-
this.webpMetadata.BackgroundColor = backgroundColor;
120-
119+
bool ignoreMetadata = this.skipMetadata;
120+
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
121121
Span<byte> buffer = stackalloc byte[4];
122122
uint frameCount = 0;
123123
int remainingBytes = (int)completeDataSize;
@@ -135,9 +135,16 @@ public ImageInfo Identify(
135135

136136
remainingBytes -= (int)dataSize;
137137
break;
138+
case WebpChunkType.Iccp:
138139
case WebpChunkType.Xmp:
139140
case WebpChunkType.Exif:
140-
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, this.metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer);
141+
WebpChunkParsingUtils.ParseOptionalChunks(
142+
stream,
143+
chunkType,
144+
this.metadata,
145+
ignoreMetadata,
146+
segmentIntegrityHandling,
147+
buffer);
141148
break;
142149
default:
143150

@@ -187,9 +194,12 @@ public Image<TPixel> Decode<TPixel>(
187194
this.webpMetadata.BackgroundColor = backgroundColor;
188195
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
189196

197+
bool ignoreMetadata = this.skipMetadata;
198+
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
190199
Span<byte> buffer = stackalloc byte[4];
191200
uint frameCount = 0;
192201
int remainingBytes = (int)completeDataSize;
202+
193203
while (remainingBytes > 0)
194204
{
195205
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
@@ -209,9 +219,10 @@ public Image<TPixel> Decode<TPixel>(
209219

210220
remainingBytes -= (int)dataSize;
211221
break;
222+
case WebpChunkType.Iccp:
212223
case WebpChunkType.Xmp:
213224
case WebpChunkType.Exif:
214-
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer);
225+
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
215226
break;
216227
default:
217228

src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

Lines changed: 152 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using SixLabors.ImageSharp.Memory;
1010
using SixLabors.ImageSharp.Metadata;
1111
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
12+
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
1213
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
1314

1415
namespace SixLabors.ImageSharp.Formats.Webp;
@@ -275,7 +276,7 @@ public static uint ReadUInt24LittleEndian(Stream stream, Span<byte> buffer)
275276
/// <summary>
276277
/// Writes a unsigned 24 bit integer.
277278
/// </summary>
278-
/// <param name="stream">The stream to read from.</param>
279+
/// <param name="stream">The stream to write to.</param>
279280
/// <param name="data">The uint24 data to write.</param>
280281
/// <exception cref="InvalidDataException">
281282
/// Thrown if the data is not a valid unsigned 24 bit integer.
@@ -337,7 +338,8 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte>
337338
return (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
338339
}
339340

340-
// We should ignore unknown chunks but still be able to read the type.
341+
// While we ignore unknown chunks we still need a to be a ble to read a chunk type
342+
// known or otherwise from the stream.
341343
throw new ImageFormatException("Invalid Webp data, could not read chunk type.");
342344
}
343345

@@ -349,88 +351,179 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte>
349351
/// <param name="stream">The stream to read the data from.</param>
350352
/// <param name="chunkType">The chunk type to parse.</param>
351353
/// <param name="metadata">The image metadata to write to.</param>
352-
/// <param name="ignoreMetaData">If true, metadata will be ignored.</param>
354+
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
353355
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
354356
/// <param name="buffer">Buffer to store the data read from the stream.</param>
355357
public static void ParseOptionalChunks(
356358
BufferedReadStream stream,
357359
WebpChunkType chunkType,
358360
ImageMetadata metadata,
359-
bool ignoreMetaData,
361+
bool ignoreMetadata,
360362
SegmentIntegrityHandling segmentIntegrityHandling,
361363
Span<byte> buffer)
362364
{
363-
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
364365
long streamLength = stream.Length;
365366
while (stream.Position < streamLength)
366367
{
367-
// Ignore unknown chunk types or when metadata is to be ignored.
368-
// If handling should ignore none, we still need to validate the chunk length.
369-
uint chunkLength = ReadChunkSize(stream, buffer, ignoreNone && (chunkType is WebpChunkType.Exif or WebpChunkType.Xmp) && !ignoreMetaData);
370-
371-
if (ignoreMetaData)
372-
{
373-
stream.Skip((int)chunkLength);
374-
}
375-
376-
int bytesRead;
377368
switch (chunkType)
378369
{
379-
case WebpChunkType.Exif:
380-
byte[] exifData = new byte[chunkLength];
381-
bytesRead = stream.Read(exifData, 0, (int)chunkLength);
382-
if (bytesRead != chunkLength)
383-
{
384-
if (ignoreNone)
385-
{
386-
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
387-
}
388-
389-
return;
390-
}
391-
392-
if (metadata.ExifProfile == null)
393-
{
394-
ExifProfile exifProfile = new(exifData);
395-
396-
// Set the resolution from the metadata.
397-
double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution);
398-
double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution);
399-
400-
if (horizontalValue > 0 && verticalValue > 0)
401-
{
402-
metadata.HorizontalResolution = horizontalValue;
403-
metadata.VerticalResolution = verticalValue;
404-
metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile);
405-
}
406-
407-
metadata.ExifProfile = exifProfile;
408-
}
370+
case WebpChunkType.Iccp:
371+
ReadIccProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
372+
break;
409373

374+
case WebpChunkType.Exif:
375+
ReadExifProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
410376
break;
411377
case WebpChunkType.Xmp:
412-
byte[] xmpData = new byte[chunkLength];
413-
bytesRead = stream.Read(xmpData, 0, (int)chunkLength);
414-
if (bytesRead != chunkLength)
415-
{
416-
if (ignoreNone)
417-
{
418-
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
419-
}
420-
421-
return;
422-
}
423-
424-
metadata.XmpProfile ??= new XmpProfile(xmpData);
425-
378+
ReadXmpProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
426379
break;
427380
default:
381+
382+
// Ignore unknown chunks.
383+
// These must always fall after the image data so we are safe to always skip them.
384+
uint chunkLength = ReadChunkSize(stream, buffer, false);
428385
stream.Skip((int)chunkLength);
429386
break;
430387
}
431388
}
432389
}
433390

391+
/// <summary>
392+
/// Reads the ICCP chunk from the stream.
393+
/// </summary>
394+
/// <param name="stream">The stream to decode from.</param>
395+
/// <param name="metadata">The image metadata.</param>
396+
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
397+
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
398+
/// <param name="buffer">Temporary buffer.</param>
399+
public static void ReadIccProfile(
400+
BufferedReadStream stream,
401+
ImageMetadata metadata,
402+
bool ignoreMetadata,
403+
SegmentIntegrityHandling segmentIntegrityHandling,
404+
Span<byte> buffer)
405+
{
406+
// While ICC profiles are optional, an invalid ICC profile cannot be ignored as it must precede the image data
407+
// and since we canot determine its size to allow skipping without reading the chunk size, we have to throw if it's invalid.
408+
// Hence we do not consider segment integrity handling here.
409+
uint iccpChunkSize = ReadChunkSize(stream, buffer);
410+
if (ignoreMetadata || metadata.IccProfile != null)
411+
{
412+
stream.Skip((int)iccpChunkSize);
413+
}
414+
else
415+
{
416+
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
417+
byte[] iccpData = new byte[iccpChunkSize];
418+
int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize);
419+
420+
// We have the size but the profile is invalid if we cannot read enough data.
421+
// Use the segment integrity handling to determine if we throw.
422+
if (bytesRead != iccpChunkSize && ignoreNone)
423+
{
424+
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk");
425+
}
426+
427+
IccProfile profile = new(iccpData);
428+
if (profile.CheckIsValid())
429+
{
430+
metadata.IccProfile = profile;
431+
}
432+
}
433+
}
434+
435+
/// <summary>
436+
/// Reads the EXIF profile from the stream.
437+
/// </summary>
438+
/// <param name="stream">The stream to decode from.</param>
439+
/// <param name="metadata">The image metadata.</param>
440+
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
441+
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
442+
/// <param name="buffer">Temporary buffer.</param>
443+
public static void ReadExifProfile(
444+
BufferedReadStream stream,
445+
ImageMetadata metadata,
446+
bool ignoreMetadata,
447+
SegmentIntegrityHandling segmentIntegrityHandling,
448+
Span<byte> buffer)
449+
{
450+
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
451+
uint exifChunkSize = ReadChunkSize(stream, buffer, ignoreNone);
452+
if (ignoreMetadata || metadata.ExifProfile != null)
453+
{
454+
stream.Skip((int)exifChunkSize);
455+
}
456+
else
457+
{
458+
byte[] exifData = new byte[exifChunkSize];
459+
int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize);
460+
if (bytesRead != exifChunkSize)
461+
{
462+
if (ignoreNone)
463+
{
464+
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
465+
}
466+
467+
return;
468+
}
469+
470+
ExifProfile exifProfile = new(exifData);
471+
472+
// Set the resolution from the metadata.
473+
double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution);
474+
double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution);
475+
476+
if (horizontalValue > 0 && verticalValue > 0)
477+
{
478+
metadata.HorizontalResolution = horizontalValue;
479+
metadata.VerticalResolution = verticalValue;
480+
metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile);
481+
}
482+
483+
metadata.ExifProfile = exifProfile;
484+
}
485+
}
486+
487+
/// <summary>
488+
/// Reads the XMP profile the stream.
489+
/// </summary>
490+
/// <param name="stream">The stream to decode from.</param>
491+
/// <param name="metadata">The image metadata.</param>
492+
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
493+
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
494+
/// <param name="buffer">Temporary buffer.</param>
495+
public static void ReadXmpProfile(
496+
BufferedReadStream stream,
497+
ImageMetadata metadata,
498+
bool ignoreMetadata,
499+
SegmentIntegrityHandling segmentIntegrityHandling,
500+
Span<byte> buffer)
501+
{
502+
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
503+
504+
uint xmpChunkSize = ReadChunkSize(stream, buffer, ignoreNone);
505+
if (ignoreMetadata || metadata.XmpProfile != null)
506+
{
507+
stream.Skip((int)xmpChunkSize);
508+
}
509+
else
510+
{
511+
byte[] xmpData = new byte[xmpChunkSize];
512+
int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize);
513+
if (bytesRead != xmpChunkSize)
514+
{
515+
if (ignoreNone)
516+
{
517+
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
518+
}
519+
520+
return;
521+
}
522+
523+
metadata.XmpProfile = new XmpProfile(xmpData);
524+
}
525+
}
526+
434527
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)
435528
{
436529
if (exifProfile.TryGetValue(tag, out IExifValue<Rational>? resolution))

0 commit comments

Comments
 (0)