Browse Source

Merge pull request #3055 from SixLabors/js/tiff-palette-metadata

Capture palette from Tiff when available.
main
James Jackson-South 5 days ago
committed by GitHub
parent
commit
e24be5cb2a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 19
      src/ImageSharp/Color/Color.cs
  2. 25
      src/ImageSharp/Formats/Tiff/Constants/TiffExtraSamples.cs
  3. 116
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs
  4. 2
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
  5. 25
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  6. 2
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  7. 12
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  8. 6
      tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs
  9. 22
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

19
src/ImageSharp/Color/Color.cs

@ -96,6 +96,21 @@ public readonly partial struct Color : IEquatable<Color>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromScaledVector(Vector4 source) => new(source);
/// <summary>
/// Bulk converts a span of generic scaled <see cref="Vector4"/> to a span of <see cref="Color"/>.
/// </summary>
/// <param name="source">The source vector span.</param>
/// <param name="destination">The destination color span.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromScaledVector(ReadOnlySpan<Vector4> source, Span<Color> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector(source[i]);
}
}
/// <summary>
/// Bulk converts a span of a specified <typeparamref name="TPixel"/> type to a span of <see cref="Color"/>.
/// </summary>
@ -112,14 +127,14 @@ public readonly partial struct Color : IEquatable<Color>
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
{
for (int i = 0; i < destination.Length; i++)
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector(source[i].ToScaledVector4());
}
}
else
{
for (int i = 0; i < destination.Length; i++)
for (int i = 0; i < source.Length; i++)
{
destination[i] = new Color(source[i]);
}

25
src/ImageSharp/Formats/Tiff/Constants/TiffExtraSamples.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants;
/// <summary>
/// Enumeration representing the possible uses of extra components in TIFF format files.
/// </summary>
internal enum TiffExtraSamples
{
/// <summary>
/// Unspecified data.
/// </summary>
Unspecified = 0,
/// <summary>
/// Associated alpha data (with pre-multiplied color).
/// </summary>
AssociatedAlpha = 1,
/// <summary>
/// Unassociated alpha data.
/// </summary>
UnassociatedAlpha = 2
}

116
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs

@ -16,8 +16,15 @@ internal class PaletteTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ushort bitsPerSample0;
private readonly ushort bitsPerSample1;
private readonly TiffExtraSampleType? extraSamplesType;
private readonly TPixel[] palette;
private readonly Vector4[] vectorPallete;
private readonly TPixel[] pixelPalette;
private readonly float alphaScale;
private readonly bool hasAlpha;
private Color[]? paletteColors;
private const float InvMax = 1f / 65535f;
@ -26,32 +33,124 @@ internal class PaletteTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
/// </summary>
/// <param name="bitsPerSample">The number of bits per sample for each pixel.</param>
/// <param name="colorMap">The RGB color lookup table to use for decoding the image.</param>
public PaletteTiffColor(TiffBitsPerSample bitsPerSample, ushort[] colorMap)
/// <param name="extraSamplesType">The type of extra samples.</param>
public PaletteTiffColor(TiffBitsPerSample bitsPerSample, ushort[] colorMap, TiffExtraSampleType? extraSamplesType)
{
this.bitsPerSample0 = bitsPerSample.Channel0;
this.bitsPerSample1 = bitsPerSample.Channel1;
this.extraSamplesType = extraSamplesType;
int colorCount = 1 << this.bitsPerSample0;
this.palette = GeneratePalette(colorMap, colorCount);
// TIFF PaletteColor uses ColorMap (tag 320 / 0x0140) which is RGB-only (no alpha).
this.vectorPallete = GenerateVectorPalette(colorMap, colorCount);
// ExtraSamples (tag 338 / 0x0152) describes extra per-pixel samples stored in the image data stream.
// For PaletteColor, any alpha is per pixel (stored alongside the index), not per palette entry.
this.hasAlpha =
this.bitsPerSample1 > 0
&& this.extraSamplesType.HasValue
&& this.extraSamplesType != TiffExtraSampleType.UnspecifiedData;
if (this.hasAlpha)
{
ulong alphaMax = (1UL << this.bitsPerSample1) - 1;
this.alphaScale = alphaMax > 0 ? 1f / alphaMax : 1f;
this.pixelPalette = [];
}
else
{
// Pre-generate pixel palette for non-alpha case for performance.
this.pixelPalette = GeneratePixelPalette(colorMap, colorCount);
}
}
public Color[] PaletteColors => this.paletteColors ??= GenerateColorPalette(this.vectorPallete);
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
BitReader bitReader = new(data);
if (this.hasAlpha)
{
Color[] colors = this.paletteColors ??= GenerateColorPalette(this.vectorPallete);
// NOTE: ExtraSamples may report "AssociatedAlphaData". For PaletteColor, the stored color sample is the
// palette index, not per-pixel RGB components, so the premultiplication concept is not representable
// in the encoded stream. We therefore treat the alpha sample as a per-pixel alpha value applied after
// palette expansion.
for (int y = top; y < top + height; y++)
{
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
for (int x = 0; x < pixelRow.Length; x++)
{
int index = bitReader.ReadBits(this.bitsPerSample0);
float alpha = bitReader.ReadBits(this.bitsPerSample1) * this.alphaScale;
// Defensive guard against malformed streams.
if ((uint)index >= (uint)this.vectorPallete.Length)
{
index = 0;
}
Vector4 color = this.vectorPallete[index];
color.W = alpha;
pixelRow[x] = TPixel.FromScaledVector4(color);
// Best-effort palette update for downstream conversions.
// This is intentionally "last writer wins" with no per-pixel branch.
// Performance is not an issue here since the constructor performs no actual transformations.
colors[index] = Color.FromScaledVector(color);
}
bitReader.NextRow();
}
return;
}
for (int y = top; y < top + height; y++)
{
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
for (int x = 0; x < pixelRow.Length; x++)
{
int index = bitReader.ReadBits(this.bitsPerSample0);
pixelRow[x] = this.palette[index];
// Defensive guard against malformed streams.
if ((uint)index >= (uint)this.pixelPalette.Length)
{
index = 0;
}
pixelRow[x] = this.pixelPalette[index];
}
bitReader.NextRow();
}
}
private static TPixel[] GeneratePalette(ushort[] colorMap, int colorCount)
private static Vector4[] GenerateVectorPalette(ushort[] colorMap, int colorCount)
{
Vector4[] palette = new Vector4[colorCount];
const int rOffset = 0;
int gOffset = colorCount;
int bOffset = colorCount * 2;
for (int i = 0; i < palette.Length; i++)
{
float r = colorMap[rOffset + i] * InvMax;
float g = colorMap[gOffset + i] * InvMax;
float b = colorMap[bOffset + i] * InvMax;
palette[i] = new Vector4(r, g, b, 1f);
}
return palette;
}
private static TPixel[] GeneratePixelPalette(ushort[] colorMap, int colorCount)
{
TPixel[] palette = new TPixel[colorCount];
@ -69,4 +168,11 @@ internal class PaletteTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
return palette;
}
private static Color[] GenerateColorPalette(Vector4[] palette)
{
Color[] colors = new Color[palette.Length];
Color.FromScaledVector(palette, colors);
return colors;
}
}

2
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs

@ -387,7 +387,7 @@ internal static class TiffColorDecoderFactory<TPixel>
case TiffColorType.PaletteColor:
DebugGuard.NotNull(colorMap, "colorMap");
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap);
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap, extraSampleType);
case TiffColorType.YCbCr:
DebugGuard.IsTrue(

25
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -584,6 +584,15 @@ internal class TiffDecoderCore : ImageDecoderCore
}
}
{
// If the color decoder is the palette decoder we need to capture its palette.
if (colorDecoder is PaletteTiffColor<TPixel> paletteDecoder)
{
TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata();
tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors;
}
}
return;
}
@ -620,6 +629,15 @@ internal class TiffDecoderCore : ImageDecoderCore
colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight);
}
{
// If the color decoder is the palette decoder we need to capture its palette.
if (colorDecoder is PaletteTiffColor<TPixel> paletteDecoder)
{
TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata();
tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors;
}
}
}
/// <summary>
@ -804,6 +822,13 @@ internal class TiffDecoderCore : ImageDecoderCore
tileIndex++;
}
}
// If the color decoder is the palette decoder we need to capture its palette.
if (colorDecoder is PaletteTiffColor<TPixel> paletteDecoder)
{
TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata();
tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors;
}
}
private TiffBaseColorDecoder<TPixel> CreateChunkyColorDecoder<TPixel>(ImageFrameMetadata metadata)

2
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -407,7 +407,7 @@ internal static class TiffDecoderOptionsParser
if (exifProfile.TryGetValue(ExifTag.ColorMap, out IExifValue<ushort[]> value))
{
options.ColorMap = value.Value;
if (options.BitsPerSample.Channels != 1)
if (options.BitsPerSample.Channels is not 1 and not 2)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}

12
src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

@ -33,6 +33,11 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
this.InkSet = other.InkSet;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
if (other.LocalColorTable?.Length > 0)
{
this.LocalColorTable = other.LocalColorTable.Value.ToArray();
}
}
/// <summary>
@ -75,6 +80,11 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
/// </summary>
public int EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the local color table, if any.
/// </summary>
public ReadOnlyMemory<Color>? LocalColorTable { get; set; }
/// <inheritdoc/>
public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
@ -100,6 +110,8 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
this.LocalColorTable = null;
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);

6
tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs

@ -89,7 +89,11 @@ public class PaletteTiffColorTests : PhotometricInterpretationTestBase
public void Decode_WritesPixelData(byte[] inputData, ushort bitsPerSample, ushort[] colorMap, int left, int top, int width, int height, Rgba32[][] expectedResult)
=> AssertDecode(expectedResult, pixels =>
{
new PaletteTiffColor<Rgba32>(new TiffBitsPerSample(bitsPerSample, 0, 0), colorMap).Decode(inputData, pixels, left, top, width, height);
new PaletteTiffColor<Rgba32>(
new TiffBitsPerSample(bitsPerSample, 0, 0),
colorMap,
TiffExtraSampleType.UnspecifiedData)
.Decode(inputData, pixels, left, top, width, height);
});
private static uint[][] GeneratePalette(int count)

22
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -61,16 +61,15 @@ public class TiffMetadataTests
clone.PhotometricInterpretation = TiffPhotometricInterpretation.CieLab;
clone.Predictor = TiffPredictor.Horizontal;
Assert.False(meta.BitsPerPixel == clone.BitsPerPixel);
Assert.False(meta.Compression == clone.Compression);
Assert.False(meta.PhotometricInterpretation == clone.PhotometricInterpretation);
Assert.False(meta.Predictor == clone.Predictor);
Assert.NotEqual(meta.BitsPerPixel, clone.BitsPerPixel);
Assert.NotEqual(meta.Compression, clone.Compression);
Assert.NotEqual(meta.PhotometricInterpretation, clone.PhotometricInterpretation);
Assert.NotEqual(meta.Predictor, clone.Predictor);
}
private static void VerifyExpectedTiffFrameMetaDataIsPresent(TiffFrameMetadata frameMetaData)
{
Assert.NotNull(frameMetaData);
Assert.NotNull(frameMetaData.BitsPerPixel);
Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaData.BitsPerPixel);
Assert.Equal(TiffCompression.Lzw, frameMetaData.Compression);
Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frameMetaData.PhotometricInterpretation);
@ -409,4 +408,17 @@ public class TiffMetadataTests
// Adding the IPTC and ICC profiles dynamically increments the number of values in the original EXIF profile by 2
Assert.Equal(exifProfileInput.Values.Count + 2, encodedImageExifProfile.Values.Count);
}
[Theory]
[WithFile(PaletteDeflateMultistrip, PixelTypes.Rgba32)]
[WithFile(PaletteUncompressed, PixelTypes.Rgba32)]
public void TiffDecoder_CanAssign_ColorPalette<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance);
ImageFrame<TPixel> frame = image.Frames.RootFrame;
TiffFrameMetadata tiffMeta = frame.Metadata.GetTiffMetadata();
Assert.Equal(TiffPhotometricInterpretation.PaletteColor, tiffMeta.PhotometricInterpretation);
Assert.NotNull(tiffMeta.LocalColorTable);
}
}

Loading…
Cancel
Save