Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]
types: [ opened, synchronize, reopened ]

jobs:
# Prime a single LFS cache and expose the exact key for the matrix
Expand Down
8 changes: 4 additions & 4 deletions src/ImageSharp/Formats/DecoderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ internal bool TryGetIccProfileForColorConversion(IccProfile? profile, [NotNullWh
return false;
}

if (profile.IsCanonicalSrgbMatrixTrc())
if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
{
return false;
}

if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
if (profile.IsCanonicalSrgbMatrixTrc())
{
return false;
}
Expand All @@ -99,11 +99,11 @@ internal bool CanRemoveIccProfile(IccProfile? profile)
return false;
}

if (this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc())
if (this.ColorProfileHandling == ColorProfileHandling.Convert)
{
return true;
}

return this.ColorProfileHandling == ColorProfileHandling.Convert;
return this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc();
}
}
16 changes: 16 additions & 0 deletions src/ImageSharp/Formats/ImageDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,14 @@ private static void HandleIccProfile(DecoderOptions options, Image image)
{
image.Metadata.IccProfile = null;
}

foreach (ImageFrame frame in image.Frames)
{
if (options.CanRemoveIccProfile(frame.Metadata.IccProfile))
{
frame.Metadata.IccProfile = null;
}
}
}

private static void HandleIccProfile(DecoderOptions options, ImageInfo image)
Expand All @@ -336,5 +344,13 @@ private static void HandleIccProfile(DecoderOptions options, ImageInfo image)
{
image.Metadata.IccProfile = null;
}

foreach (ImageFrameMetadata frame in image.FrameMetadataCollection)
{
if (options.CanRemoveIccProfile(frame.IccProfile))
{
frame.IccProfile = null;
}
}
}
}
53 changes: 53 additions & 0 deletions src/ImageSharp/Formats/ImageDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;

Expand Down Expand Up @@ -164,4 +165,56 @@ protected bool TryConvertIccProfile<TPixel>(Image<TPixel> image)
converter.Convert(image);
return true;
}

/// <summary>
/// Converts the ICC color profile of the specified image frame to the compact sRGB v4 profile if a source profile is
/// available.
/// </summary>
/// <remarks>
/// This method should only be used by decoders that gurantee that the encoded image data is in a color space
/// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB).
/// <br/>
/// If the image does not have a valid ICC profile for color conversion, no changes are made.
/// This operation may affect the color appearance of the image to ensure consistency with the sRGB color
/// space.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame whose ICC profile will be converted to the compact sRGB v4 profile.</param>
/// <returns>
/// <see langword="true"/> if the conversion was performed; otherwise, <see langword="false"/>.
/// </returns>
protected bool TryConvertIccProfile<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!this.Options.TryGetIccProfileForColorConversion(frame.Metadata.IccProfile, out IccProfile? profile))
{
return false;
}

ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
MemoryAllocator = frame.Configuration.MemoryAllocator,
};

ColorProfileConverter converter = new(options);

ImageMetadata metadata = new()
{
IccProfile = frame.Metadata.IccProfile
};

IMemoryGroup<TPixel> m = frame.PixelBuffer.MemoryGroup;

// Safe: ToArray only materializes the Memory<TPixel> segment list, not the underlying pixel buffers,
// and Wrap(Memory<T>[]) creates a Consumed MemoryGroup that does not own the buffers (Dispose just
// invalidates the view). This means no pixel data is cloned and disposing the temporary image will
// not dispose or leak the frame's pixel buffer.
MemoryGroup<TPixel> memorySource = MemoryGroup<TPixel>.Wrap(m.ToArray());

using Image<TPixel> image = new(frame.Configuration, memorySource, frame.Width, frame.Height, metadata);
converter.Convert(image);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers;
using System.Numerics;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;

namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;

/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration.
/// Each channel is represented with 16 bits.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
internal class CieLab16PlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ColorProfileConverter colorProfileConverter;
private readonly Configuration configuration;
private readonly bool isBigEndian;

// libtiff encodes 16-bit Lab as:
// L* : unsigned [0, 65535] mapping to [0, 100]
// a*, b* : signed [-32768, 32767], values are 256x the 1976 a*, b* values.
private const float Inv65535 = 1f / 65535f;
private const float Inv256 = 1f / 256f;

public CieLab16PlanarTiffColor(
Configuration configuration,
DecoderOptions decoderOptions,
ImageFrameMetadata metadata,
MemoryAllocator allocator,
bool isBigEndian)
{
this.isBigEndian = isBigEndian;
this.configuration = configuration;

if (decoderOptions.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile))
{
ColorConversionOptions options = new()
{
SourceIccProfile = iccProfile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
MemoryAllocator = allocator
};

this.colorProfileConverter = new ColorProfileConverter(options);
}
else
{
ColorConversionOptions options = new()
{
MemoryAllocator = allocator
};

this.colorProfileConverter = new ColorProfileConverter(options);
}
}

/// <inheritdoc/>
public override void Decode(IMemoryOwner<byte>[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
Span<byte> lPlane = data[0].GetSpan();
Span<byte> aPlane = data[1].GetSpan();
Span<byte> bPlane = data[2].GetSpan();

// Allocate temporary buffers to hold the LAB -> RGB conversion.
// This should be the maximum width of a row.
using IMemoryOwner<Rgb> rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Rgb>(width);
using IMemoryOwner<Vector4> vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Vector4>(width);

Span<Rgb> rgbRow = rgbBuffer.Memory.Span;
Span<Vector4> vectorRow = vectorBuffer.Memory.Span;

// Reuse the rgbRow span for lab data since both are 3-float structs, avoiding an extra allocation.
Span<CieLab> cieLabRow = MemoryMarshal.Cast<Rgb, CieLab>(rgbRow);

int stride = width * 2;

if (this.isBigEndian)
{
for (int y = 0; y < height; y++)
{
int rowBase = y * stride;
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(top + y).Slice(left, width);

for (int x = 0; x < width; x++)
{
int i = rowBase + (x * 2);

ushort lRaw = TiffUtilities.ConvertToUShortBigEndian(lPlane.Slice(i, 2));
short aRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(aPlane.Slice(i, 2)));
short bRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(bPlane.Slice(i, 2)));

float l = lRaw * 100f * Inv65535;
float a = aRaw * Inv256;
float b = bRaw * Inv256;

cieLabRow[x] = new CieLab(l, a, b);
}

// Convert CIE Lab -> Rgb -> Vector4 -> TPixel
this.colorProfileConverter.Convert<CieLab, Rgb>(cieLabRow, rgbRow);
Rgb.ToScaledVector4(rgbRow, vectorRow);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
}

return;
}

for (int y = 0; y < height; y++)
{
int rowBase = y * stride;
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(top + y).Slice(left, width);

for (int x = 0; x < width; x++)
{
int i = rowBase + (x * 2);

ushort lRaw = TiffUtilities.ConvertToUShortLittleEndian(lPlane.Slice(i, 2));
short aRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(aPlane.Slice(i, 2)));
short bRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(bPlane.Slice(i, 2)));

float l = lRaw * 100f * Inv65535;
float a = aRaw * Inv256;
float b = bRaw * Inv256;

cieLabRow[x] = new CieLab(l, a, b);
}

// Convert CIE Lab -> Rgb -> Vector4 -> TPixel
this.colorProfileConverter.Convert<CieLab, Rgb>(cieLabRow, rgbRow);
Rgb.ToScaledVector4(rgbRow, vectorRow);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
}
}
}
Loading
Loading