Skip to content

Commit e24be5c

Browse files
Merge pull request #3055 from SixLabors/js/tiff-palette-metadata
Capture palette from Tiff when available.
2 parents 4d732f1 + c43b636 commit e24be5c

9 files changed

Lines changed: 189 additions & 40 deletions

File tree

src/ImageSharp/Color/Color.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ public static Color FromPixel<TPixel>(TPixel source)
9696
[MethodImpl(MethodImplOptions.AggressiveInlining)]
9797
public static Color FromScaledVector(Vector4 source) => new(source);
9898

99+
/// <summary>
100+
/// Bulk converts a span of generic scaled <see cref="Vector4"/> to a span of <see cref="Color"/>.
101+
/// </summary>
102+
/// <param name="source">The source vector span.</param>
103+
/// <param name="destination">The destination color span.</param>
104+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105+
public static void FromScaledVector(ReadOnlySpan<Vector4> source, Span<Color> destination)
106+
{
107+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
108+
for (int i = 0; i < source.Length; i++)
109+
{
110+
destination[i] = FromScaledVector(source[i]);
111+
}
112+
}
113+
99114
/// <summary>
100115
/// Bulk converts a span of a specified <typeparamref name="TPixel"/> type to a span of <see cref="Color"/>.
101116
/// </summary>
@@ -112,14 +127,14 @@ public static void FromPixel<TPixel>(ReadOnlySpan<TPixel> source, Span<Color> de
112127
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
113128
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
114129
{
115-
for (int i = 0; i < destination.Length; i++)
130+
for (int i = 0; i < source.Length; i++)
116131
{
117132
destination[i] = FromScaledVector(source[i].ToScaledVector4());
118133
}
119134
}
120135
else
121136
{
122-
for (int i = 0; i < destination.Length; i++)
137+
for (int i = 0; i < source.Length; i++)
123138
{
124139
destination[i] = new Color(source[i]);
125140
}

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

Lines changed: 0 additions & 25 deletions
This file was deleted.

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

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ internal class PaletteTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
1616
where TPixel : unmanaged, IPixel<TPixel>
1717
{
1818
private readonly ushort bitsPerSample0;
19+
private readonly ushort bitsPerSample1;
20+
private readonly TiffExtraSampleType? extraSamplesType;
1921

20-
private readonly TPixel[] palette;
22+
private readonly Vector4[] vectorPallete;
23+
private readonly TPixel[] pixelPalette;
24+
25+
private readonly float alphaScale;
26+
private readonly bool hasAlpha;
27+
private Color[]? paletteColors;
2128

2229
private const float InvMax = 1f / 65535f;
2330

@@ -26,32 +33,124 @@ internal class PaletteTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
2633
/// </summary>
2734
/// <param name="bitsPerSample">The number of bits per sample for each pixel.</param>
2835
/// <param name="colorMap">The RGB color lookup table to use for decoding the image.</param>
29-
public PaletteTiffColor(TiffBitsPerSample bitsPerSample, ushort[] colorMap)
36+
/// <param name="extraSamplesType">The type of extra samples.</param>
37+
public PaletteTiffColor(TiffBitsPerSample bitsPerSample, ushort[] colorMap, TiffExtraSampleType? extraSamplesType)
3038
{
3139
this.bitsPerSample0 = bitsPerSample.Channel0;
40+
this.bitsPerSample1 = bitsPerSample.Channel1;
41+
this.extraSamplesType = extraSamplesType;
42+
3243
int colorCount = 1 << this.bitsPerSample0;
33-
this.palette = GeneratePalette(colorMap, colorCount);
44+
45+
// TIFF PaletteColor uses ColorMap (tag 320 / 0x0140) which is RGB-only (no alpha).
46+
this.vectorPallete = GenerateVectorPalette(colorMap, colorCount);
47+
48+
// ExtraSamples (tag 338 / 0x0152) describes extra per-pixel samples stored in the image data stream.
49+
// For PaletteColor, any alpha is per pixel (stored alongside the index), not per palette entry.
50+
this.hasAlpha =
51+
this.bitsPerSample1 > 0
52+
&& this.extraSamplesType.HasValue
53+
&& this.extraSamplesType != TiffExtraSampleType.UnspecifiedData;
54+
55+
if (this.hasAlpha)
56+
{
57+
ulong alphaMax = (1UL << this.bitsPerSample1) - 1;
58+
this.alphaScale = alphaMax > 0 ? 1f / alphaMax : 1f;
59+
this.pixelPalette = [];
60+
}
61+
else
62+
{
63+
// Pre-generate pixel palette for non-alpha case for performance.
64+
this.pixelPalette = GeneratePixelPalette(colorMap, colorCount);
65+
}
3466
}
3567

68+
public Color[] PaletteColors => this.paletteColors ??= GenerateColorPalette(this.vectorPallete);
69+
3670
/// <inheritdoc/>
3771
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
3872
{
3973
BitReader bitReader = new(data);
4074

75+
if (this.hasAlpha)
76+
{
77+
Color[] colors = this.paletteColors ??= GenerateColorPalette(this.vectorPallete);
78+
79+
// NOTE: ExtraSamples may report "AssociatedAlphaData". For PaletteColor, the stored color sample is the
80+
// palette index, not per-pixel RGB components, so the premultiplication concept is not representable
81+
// in the encoded stream. We therefore treat the alpha sample as a per-pixel alpha value applied after
82+
// palette expansion.
83+
for (int y = top; y < top + height; y++)
84+
{
85+
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
86+
for (int x = 0; x < pixelRow.Length; x++)
87+
{
88+
int index = bitReader.ReadBits(this.bitsPerSample0);
89+
float alpha = bitReader.ReadBits(this.bitsPerSample1) * this.alphaScale;
90+
91+
// Defensive guard against malformed streams.
92+
if ((uint)index >= (uint)this.vectorPallete.Length)
93+
{
94+
index = 0;
95+
}
96+
97+
Vector4 color = this.vectorPallete[index];
98+
color.W = alpha;
99+
100+
pixelRow[x] = TPixel.FromScaledVector4(color);
101+
102+
// Best-effort palette update for downstream conversions.
103+
// This is intentionally "last writer wins" with no per-pixel branch.
104+
// Performance is not an issue here since the constructor performs no actual transformations.
105+
colors[index] = Color.FromScaledVector(color);
106+
}
107+
108+
bitReader.NextRow();
109+
}
110+
111+
return;
112+
}
113+
41114
for (int y = top; y < top + height; y++)
42115
{
43116
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
44117
for (int x = 0; x < pixelRow.Length; x++)
45118
{
46119
int index = bitReader.ReadBits(this.bitsPerSample0);
47-
pixelRow[x] = this.palette[index];
120+
121+
// Defensive guard against malformed streams.
122+
if ((uint)index >= (uint)this.pixelPalette.Length)
123+
{
124+
index = 0;
125+
}
126+
127+
pixelRow[x] = this.pixelPalette[index];
48128
}
49129

50130
bitReader.NextRow();
51131
}
52132
}
53133

54-
private static TPixel[] GeneratePalette(ushort[] colorMap, int colorCount)
134+
private static Vector4[] GenerateVectorPalette(ushort[] colorMap, int colorCount)
135+
{
136+
Vector4[] palette = new Vector4[colorCount];
137+
138+
const int rOffset = 0;
139+
int gOffset = colorCount;
140+
int bOffset = colorCount * 2;
141+
142+
for (int i = 0; i < palette.Length; i++)
143+
{
144+
float r = colorMap[rOffset + i] * InvMax;
145+
float g = colorMap[gOffset + i] * InvMax;
146+
float b = colorMap[bOffset + i] * InvMax;
147+
palette[i] = new Vector4(r, g, b, 1f);
148+
}
149+
150+
return palette;
151+
}
152+
153+
private static TPixel[] GeneratePixelPalette(ushort[] colorMap, int colorCount)
55154
{
56155
TPixel[] palette = new TPixel[colorCount];
57156

@@ -69,4 +168,11 @@ private static TPixel[] GeneratePalette(ushort[] colorMap, int colorCount)
69168

70169
return palette;
71170
}
171+
172+
private static Color[] GenerateColorPalette(Vector4[] palette)
173+
{
174+
Color[] colors = new Color[palette.Length];
175+
Color.FromScaledVector(palette, colors);
176+
return colors;
177+
}
72178
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ public static TiffBaseColorDecoder<TPixel> Create(
387387

388388
case TiffColorType.PaletteColor:
389389
DebugGuard.NotNull(colorMap, "colorMap");
390-
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap);
390+
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap, extraSampleType);
391391

392392
case TiffColorType.YCbCr:
393393
DebugGuard.IsTrue(

src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,15 @@ private void DecodeStripsChunky<TPixel>(
584584
}
585585
}
586586

587+
{
588+
// If the color decoder is the palette decoder we need to capture its palette.
589+
if (colorDecoder is PaletteTiffColor<TPixel> paletteDecoder)
590+
{
591+
TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata();
592+
tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors;
593+
}
594+
}
595+
587596
return;
588597
}
589598

@@ -620,6 +629,15 @@ private void DecodeStripsChunky<TPixel>(
620629

621630
colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight);
622631
}
632+
633+
{
634+
// If the color decoder is the palette decoder we need to capture its palette.
635+
if (colorDecoder is PaletteTiffColor<TPixel> paletteDecoder)
636+
{
637+
TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata();
638+
tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors;
639+
}
640+
}
623641
}
624642

625643
/// <summary>
@@ -804,6 +822,13 @@ private void DecodeTilesChunky<TPixel>(
804822
tileIndex++;
805823
}
806824
}
825+
826+
// If the color decoder is the palette decoder we need to capture its palette.
827+
if (colorDecoder is PaletteTiffColor<TPixel> paletteDecoder)
828+
{
829+
TiffFrameMetadata tiffFrameMetadata = frame.Metadata.GetTiffMetadata();
830+
tiffFrameMetadata.LocalColorTable = paletteDecoder.PaletteColors;
831+
}
807832
}
808833

809834
private TiffBaseColorDecoder<TPixel> CreateChunkyColorDecoder<TPixel>(ImageFrameMetadata metadata)

src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ private static void ParseColorType(this TiffDecoderCore options, ExifProfile exi
407407
if (exifProfile.TryGetValue(ExifTag.ColorMap, out IExifValue<ushort[]> value))
408408
{
409409
options.ColorMap = value.Value;
410-
if (options.BitsPerSample.Channels != 1)
410+
if (options.BitsPerSample.Channels is not 1 and not 2)
411411
{
412412
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
413413
}

src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ private TiffFrameMetadata(TiffFrameMetadata other)
3333
this.InkSet = other.InkSet;
3434
this.EncodingWidth = other.EncodingWidth;
3535
this.EncodingHeight = other.EncodingHeight;
36+
37+
if (other.LocalColorTable?.Length > 0)
38+
{
39+
this.LocalColorTable = other.LocalColorTable.Value.ToArray();
40+
}
3641
}
3742

3843
/// <summary>
@@ -75,6 +80,11 @@ private TiffFrameMetadata(TiffFrameMetadata other)
7580
/// </summary>
7681
public int EncodingHeight { get; set; }
7782

83+
/// <summary>
84+
/// Gets or sets the local color table, if any.
85+
/// </summary>
86+
public ReadOnlyMemory<Color>? LocalColorTable { get; set; }
87+
7888
/// <inheritdoc/>
7989
public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
8090
{
@@ -100,6 +110,8 @@ public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
100110
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
101111
where TPixel : unmanaged, IPixel<TPixel>
102112
{
113+
this.LocalColorTable = null;
114+
103115
float ratioX = destination.Width / (float)source.Width;
104116
float ratioY = destination.Height / (float)source.Height;
105117
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ public static IEnumerable<object[]> Palette8Data
8989
public void Decode_WritesPixelData(byte[] inputData, ushort bitsPerSample, ushort[] colorMap, int left, int top, int width, int height, Rgba32[][] expectedResult)
9090
=> AssertDecode(expectedResult, pixels =>
9191
{
92-
new PaletteTiffColor<Rgba32>(new TiffBitsPerSample(bitsPerSample, 0, 0), colorMap).Decode(inputData, pixels, left, top, width, height);
92+
new PaletteTiffColor<Rgba32>(
93+
new TiffBitsPerSample(bitsPerSample, 0, 0),
94+
colorMap,
95+
TiffExtraSampleType.UnspecifiedData)
96+
.Decode(inputData, pixels, left, top, width, height);
9397
});
9498

9599
private static uint[][] GeneratePalette(int count)

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,15 @@ public void TiffFrameMetadata_CloneIsDeep<TPixel>(TestImageProvider<TPixel> prov
6161
clone.PhotometricInterpretation = TiffPhotometricInterpretation.CieLab;
6262
clone.Predictor = TiffPredictor.Horizontal;
6363

64-
Assert.False(meta.BitsPerPixel == clone.BitsPerPixel);
65-
Assert.False(meta.Compression == clone.Compression);
66-
Assert.False(meta.PhotometricInterpretation == clone.PhotometricInterpretation);
67-
Assert.False(meta.Predictor == clone.Predictor);
64+
Assert.NotEqual(meta.BitsPerPixel, clone.BitsPerPixel);
65+
Assert.NotEqual(meta.Compression, clone.Compression);
66+
Assert.NotEqual(meta.PhotometricInterpretation, clone.PhotometricInterpretation);
67+
Assert.NotEqual(meta.Predictor, clone.Predictor);
6868
}
6969

7070
private static void VerifyExpectedTiffFrameMetaDataIsPresent(TiffFrameMetadata frameMetaData)
7171
{
7272
Assert.NotNull(frameMetaData);
73-
Assert.NotNull(frameMetaData.BitsPerPixel);
7473
Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaData.BitsPerPixel);
7574
Assert.Equal(TiffCompression.Lzw, frameMetaData.Compression);
7675
Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frameMetaData.PhotometricInterpretation);
@@ -409,4 +408,17 @@ public void Encode_PreservesMetadata_IptcAndIcc<TPixel>(TestImageProvider<TPixel
409408
// Adding the IPTC and ICC profiles dynamically increments the number of values in the original EXIF profile by 2
410409
Assert.Equal(exifProfileInput.Values.Count + 2, encodedImageExifProfile.Values.Count);
411410
}
411+
412+
[Theory]
413+
[WithFile(PaletteDeflateMultistrip, PixelTypes.Rgba32)]
414+
[WithFile(PaletteUncompressed, PixelTypes.Rgba32)]
415+
public void TiffDecoder_CanAssign_ColorPalette<TPixel>(TestImageProvider<TPixel> provider)
416+
where TPixel : unmanaged, IPixel<TPixel>
417+
{
418+
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance);
419+
ImageFrame<TPixel> frame = image.Frames.RootFrame;
420+
TiffFrameMetadata tiffMeta = frame.Metadata.GetTiffMetadata();
421+
Assert.Equal(TiffPhotometricInterpretation.PaletteColor, tiffMeta.PhotometricInterpretation);
422+
Assert.NotNull(tiffMeta.LocalColorTable);
423+
}
412424
}

0 commit comments

Comments
 (0)