Skip to content

Add ICC profiles color spaces and output intent support, and add tests - #1383

Open
BobLd wants to merge 13 commits into
UglyToad:masterfrom
BobLd:feature/icc-profile-support-5-split
Open

Add ICC profiles color spaces and output intent support, and add tests#1383
BobLd wants to merge 13 commits into
UglyToad:masterfrom
BobLd:feature/icc-profile-support-5-split

Conversation

@BobLd

@BobLd BobLd commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

The code changes were made with AI assistance.

PdfPig can now support ICC profiles for colour management.

ParsingOptions.IccProfileService now lets a caller plug in a colour management module (like Unicolour). No ICC profile management is done in PdfPig.

When one is configured, /ICCBased colour spaces convert through their embedded profile instead of always deferring to the alternate. A document's output intents are parsed, exposed on the graphics state, and, when the service asks for it, applied: device colours (DeviceGray, DeviceRGB, DeviceCMYK) are converted through the output intent's profile rather than the built-in approximations. Rendering intent is now passed through every colour conversion, and images take their default /Decode array from the colour space, which fixes Lab images decoding near-black.

Without an ICC profile service configured, behaviour is unchanged.

Breaking changes

1. CurrentGraphicsState colour setters removed

CurrentStrokingColor and CurrentNonStrokingColor are now [Obsolete(..., error: true)] on the setter, and throw NotSupportedException if reached by an assembly compiled against an older version.

Why. A colour assigned straight into the graphics state carries no colour space and no operands, so it cannot answer a later ri operator and would silently pin itself to whatever intent happened to be in force.

// before
state.CurrentNonStrokingColor = new RGBColor(1, 0, 0);
state.CurrentStrokingColor = CMYKColor.Black;

// after, with an already-converted colour
state.SetNonStrokingColor(new RGBColor(1, 0, 0));
state.SetStrokingColor(CMYKColor.Black);

// after, with operands that stay reconvertible when the intent changes
state.SetNonStrokingColor(DeviceRgbColorSpaceDetails.Instance, [1.0, 0.0, 0.0]);

Getters are unchanged.


2. BaseStreamProcessor<T> constructor gained pageDictionary

Affects custom stream processors.

protected MyStreamProcessor(
    IResourceStore resourceStore,
    IPdfTokenScanner pdfScanner,
    IPageContentParser pageContentParser,
    ILookupFilterProvider filterProvider,
    CropBox cropBox,
    UserSpaceUnit userSpaceUnit,
    PageRotationDegrees rotation,
    in TransformationMatrix initialMatrix,
+   DictionaryToken? pageDictionary,
    ParsingOptions parsingOptions)
    : base(resourceStore, pdfScanner, pageContentParser, filterProvider,
           cropBox, userSpaceUnit, rotation, initialMatrix,
+          pageDictionary,
           parsingOptions)

Pass the page dictionary so page-level /OutputIntents (PDF 2.0, Table 31) are picked up; pass null if you have none, and the document catalog's intents are used instead.


3. XObjectFactory.ReadImage gained ParsingOptions

- XObjectFactory.ReadImage(record, scanner, filterProvider, resourceStore);
+ XObjectFactory.ReadImage(record, scanner, filterProvider, resourceStore, options);

Inside a BaseStreamProcessor subclass, ParsingOptions is already available as a property, so the call becomes ..., resourceStore, ParsingOptions).

The options carry IccProfileService, which is what lets a JPEG 2000 image decode the ICC profile embedded in its own codestream.


4. ColorSpaceDetailsByteConverter.Convert 6-argument overload gained RenderingIntent

- ColorSpaceDetailsByteConverter.Convert(details, bytes, bpc, width, height, image.Decode);
+ ColorSpaceDetailsByteConverter.Convert(details, bytes, bpc, width, height, image.Decode,
+                                        image.RenderingIntent);

5. IResourceStore gained four members

Only breaking if you implement the interface yourself. Minimal opt-out implementations:

public IIccProfileService? IccProfileService => null;   // no ICC colour management
public ILog Logger => new NoOpLog();

public IReadOnlyList<OutputIntent> DocumentOutputIntents => [];

public IReadOnlyList<OutputIntent> GetPageOutputIntents(DictionaryToken? pageDictionary)
    => [];

Returning null/empty everywhere reproduces the previous behaviour exactly: ICC-based colour spaces fall back to their alternate, and no output intent is in effect.


6. InlineImageBuilder constructor now requires ParsingOptions

The implicit public parameterless constructor is gone, replaced by public InlineImageBuilder(ParsingOptions options).

- var builder = new InlineImageBuilder();
+ var builder = new InlineImageBuilder(parsingOptions);

The options are needed so an inline image's soft mask can be read through XObjectFactory.ReadImage, which now takes them (see item 3).


7. IIccProfileService carries the output intent policy

Applying an output intent is impossible without a service to parse its profile, so the capability and the decision to use it are the same thing and live together:

public bool TryGetProfile(ReadOnlyMemory<byte> profileBytes, out IIccProfile? profile) { /* ... */ }

// Convert device colours through the document's output intent profile? 14.11.5 says a processor is
// free to disregard an output intent, so this should be false unless previewing or proofing.
public bool UseOutputIntent => false;

// Which entry to prefer when a document declares several: an /S subtype, matched exactly.
// OutputIntent.PdfXSubtype is the usual answer; null is the built-in PDF/X, PDF/A, other order.
public string? PreferredOutputIntentSubtype => null;

Answering false and null reproduces the behaviour of a service that only parses profiles: /ICCBased colour spaces are colour-managed, device colours are not.


Opting out entirely

Without ParsingOptions.IccProfileService set, no ICC colour management happens and /ICCBased spaces behave as before. With one set but answering UseOutputIntent false, /ICCBased spaces are colour-managed and device colours are left alone, which is the recommended default: 14.11.5 sets "no expectation" that device colours are converted to the output intent's target, and says such conversion is "undesirable" in some workflows.

The only behaviour that changes for every caller regardless is the default /Decode array (above).

@BobLd
BobLd force-pushed the feature/icc-profile-support-5-split branch from 828ec44 to 42e37ec Compare August 16, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant