Convert a print job to CMYK and JPEG 2000 pictures are often the only casualties: a red logo turns blue, gray drifts toward magenta, a cut-out shot lands in a black box. PDFium Component's document color conversion, ConvertPdfDocumentColours, keeps them right when the PDF /ColorSpace, JP2 colr boxes and CDEF channel definitions each decide only their own part
Who decides what inside a JPX image
A JPXDecode image describes itself: the SIZ segment carries each channel's precision and signedness, colr boxes carry color declarations, CDEF assigns channel roles, and any of them can disagree with the dictionary around it. ISO 32000-1 §7.4.9 settles who wins: a /ColorSpace in the image dictionary overrides every color specification inside the JPEG 2000 data, embedded ICC profile included, the decoder rather than /BitsPerComponent determines the depth, and PDF admits only baseline JPX features. Opacity runs on its own track, declared by CDEF and requested through the image dictionary's /SMaskInData entry (§8.9.5). ConvertPdfDocumentColours divides the work like this:
| Source | What it decides | Precedence | What the converter does |
|---|---|---|---|
PDF /ColorSpace | The meaning of the decoded color channels | Always beats the JP2 colr boxes | Uses it as declared and still looks for an opacity channel in the stream |
JP2 colr boxes | The meaning of the channels when /ColorSpace is absent | Highest PRECEDENCE, lowest APPROX among equals, file order last | Tries the candidates in that order and skips any it cannot use |
| SIZ precision | Real bit depth and signedness per channel | Beats /BitsPerComponent | Decodes to an 8-bit buffer when every channel is 8 bits or less, 16-bit otherwise |
/Decode | Nothing for an ordinary JPX image; inversion for an image mask | Ignored by §7.4.9 | Removes the entry after a successful JPX decode |
| CDEF box | Which channel is color, opacity (typ 1) or premultiplied opacity (typ 2) | Defines the alpha; with CMAP present it names channels | Divides typ 2 color back by alpha and writes the alpha as an 8-bit DeviceGray /SMask |
/SMaskInData | Whether the PDF asks for the embedded mask | Must agree with what CDEF actually contains | Accepts 1 only when an opacity channel exists, refuses 2, drops the key once /SMask is written |
The /Decode row produces the most visible damage when a converter ignores it: [1 0 1 0 1 0] on a JPX image looks like an inversion request, and applying it before the ICC transform prints a negative that conforming viewers never showed. The bit-depth row does its damage quietly. A producer can legally write /BitsPerComponent 8 over 12-bit channels or omit the key, so PDFium Component sizes its buffers from SIZ, shifts each signed channel by 2^(p-1) at its own precision p, and keeps one denominator per channel when precisions are mixed. Divide a 5-bit channel by its 12-bit neighbor's maximum and it sinks into the bottom 1% of the range
Why does a JPEG 2000 image change color after CMYK conversion?
Almost every color cast traces back to interpretation: the decoded channels were read with the wrong meaning before any ICC math ran. Without /ColorSpace, the meaning comes from the colr boxes, and a JP2 file may carry several. The JPX extension (ITU-T T.801) gives each box a signed PRECEDENCE and an APPROX accuracy grade; the converter takes the highest precedence, breaks ties with the lower APPROX, and falls back on file order. An embedded ICC profile (method 2 or 3) must match the image's channel count, describe RGB, gray or CMYK data, and be accepted by the color engine, while enumerated spaces such as sRGB (16), greyscale (17), sYCC (18), CMYK (12) and CIELab (14) have conditions of their own. A failed candidate gives way to the next, so a damaged ICC box with a valid sRGB enumeration behind it still converts. Note that sRGB 16 always means standard sRGB, whatever your DeviceRGBProfile says, and greyscale 17 follows the sRGB tone curve instead of a plain gamma 2.2
sYCC produces both the loudest and the quietest casts. The decoder undoes any codestream multi-component transform by itself; an sYCC colr box is a separate statement that the decoded values are still Y, Cb and Cr. Read them as RGB and 8-bit sRGB red (255, 0, 0), stored as about Y 76, Cb 85, Cr 255, comes out saturated blue. The quiet cast comes from the chroma midpoint, which is 2^(p-1) at the channel's original precision p, while the decoder hands back every channel scaled into a shared 8- or 16-bit buffer. Subtract 128 from that buffer and a 4-bit chroma channel is 8 levels off on both Cb and Cr; through the sYCC matrix (R = Y + 1.402 Cr, G = Y − 0.344136 Cb − 0.714136 Cr, B = Y + 1.772 Cb) a mid-gray of 128 lands near (139, 120, 142). Expected: a magenta cast on every neutral, smaller at higher precision but never zero, since 2^(p-1)/(2^p − 1) always exceeds one half by 0.5/(2^p − 1). PDFium Component maps each sample back to its original code and subtracts and normalizes with that channel's own midpoint and maximum before the matrix and the built-in sRGB profile run. ITU-T T.800 Annex I defines sYCC for unsigned channels only, so a declaration over signed channels is passed over for a lower-ranked one
When the JPX metadata is missing
All of this relies on color and opacity metadata that the native bridge, PdfiumPasImageComponents.dll, reports beyond plain pixel decoding. A bridge build without those metadata functions, such as a different copy earlier on the DLL search path, still loads and still decodes images that declare their color explicitly, and asking the decoder for the metadata interface still succeeds. Only a real inspection call answers "Image colour metadata is unavailable". The converter reports that image instead of treating it as undeclared. When inspection works and no declaration is usable, AllowJpxDeviceFallback decides whether the channel count picks DeviceGray, DeviceRGB or DeviceCMYK, resolved through the page's Default color spaces; it defaults to True and counts each such image in Report.JpxDeviceFallbackImages. For PDF/X work, set it to False. The fallback encodes an unreviewed assumption about the producer's intent, and a log counter protects nobody unless it is read before plates are made
uses
System.SysUtils, System.Classes, System.IOUtils,
FPdfProduction, FPdfImageComponents, FPdfColourLittleCms, FPdfDocumentColour;
function ConvertJobToPress(const InputFile, OutputFile, PressIccFile: string;
Log: TStrings): Boolean;
var
Source: TFileStream;
Converted: TMemoryStream;
PressProfile: TBytes;
Transform: IPdfColourTransform;
Options: TPdfDocumentColourOptions;
Report: TPdfDocumentColourReport;
Issue: TPdfDocumentColourIssue;
begin
PressProfile := TFile.ReadAllBytes(PressIccFile);
Transform := CreatePdfLittleCmsColourTransform('PdfiumPasLittleCms2.dll',
TPdfLittleCmsOptions.Default);
Options := TPdfDocumentColourOptions.Default;
Options.ImageDecoder := CreatePdfImageComponentDecoder(
'PdfiumPasImageComponents.dll', TPdfImageComponentOptions.Default);
Options.DeviceCMYKProfile := PressProfile; // untagged CMYK already targets this press
Options.AllowJpxDeviceFallback := False; // undeclared JPX color is reported, not guessed
// FailOnUnsupported keeps its default of True: one unsupported object stops the run
Source := TFileStream.Create(InputFile, fmOpenRead or fmShareDenyWrite);
try
Converted := TMemoryStream.Create;
try
Result := ConvertPdfDocumentColours(Source, Converted, pcsDeviceCMYK,
PressProfile, Transform, Options, Report);
Log.Add(Format('Images converted: %d, JPX device fallbacks: %d',
[Report.ImagesConverted, Report.JpxDeviceFallbackImages]));
for Issue in Report.Issues do
Log.Add(Format(' object %d: %s', [Issue.ObjectNumber, Issue.Reason]));
if Result then
Converted.SaveToFile(OutputFile)
else
Log.Add('Conversion refused: ' + Report.ErrorText);
finally
Converted.Free;
end;
finally
Source.Free;
end;
end;
Since PDF admits only baseline JPX, a CRG marker with nonzero component registration offsets has no place in a PDF image; the bridge reads the main header marker by marker and rejects such offsets, which OpenJPEG would otherwise decode as if they were absent. OpenJPEG 2.5 also parses a 31-bit SIZ header cleanly while returning near-midpoint samples, so the bridge accepts channel precisions up to 24 bits and reports deeper ones at inspection. When a supplier's file is refused, pass the JPXDecode payload or the original .jp2 asset to the same decoder: the routine below lists the color channels, every colr declaration and the alpha type, plus the codec versions for your support log
uses
System.SysUtils, System.StrUtils, System.Classes, System.IOUtils,
FPdfImageComponents;
procedure DescribeJpxAsset(const Decoder: IPdfImageComponentDecoder;
const FileName: string; Log: TStrings);
var
Encoded: TBytes;
Colours: IPdfImageColourMetadata;
Opacity: IPdfImageOpacity;
Info: TPdfImageComponentInfo;
Specs: TPdfImageColourSpecifications;
Alpha: TPdfImageOpacityInfo;
ErrorText: string;
I: Integer;
begin
Encoded := TFile.ReadAllBytes(FileName); // JP2 container or raw codestream
if not Supports(Decoder, IPdfImageColourMetadata, Colours) or
not Colours.InspectColours(Encoded, Info, Specs, ErrorText) then
begin
Log.Add(FileName + ': no colour metadata (' + ErrorText + ')');
Exit;
end;
Log.Add(Format('%s: %d x %d, %d colour channel(s), %d-bit buffer',
[FileName, Info.Width, Info.Height, Info.Components, Info.DecodedBitsPerComponent]));
for I := 0 to High(Info.Channels) do
Log.Add(Format(' channel %d: %d-bit %s, sampling %d x %d',
[I, Info.Channels[I].BitsPerComponent,
IfThen(Info.Channels[I].IsSigned, 'signed', 'unsigned'),
Info.Channels[I].HorizontalSampling, Info.Channels[I].VerticalSampling]));
for I := 0 to High(Specs) do
Log.Add(Format(' colr %d: method %d, precedence %d, approx %d, enum %d',
[I, Specs[I].Method, Specs[I].Precedence, Specs[I].Approximation,
Integer(Specs[I].EnumeratedColorSpace)]));
if Supports(Decoder, IPdfImageOpacity, Opacity) and
Opacity.InspectOpacity(Encoded, Alpha, ErrorText) then
Log.Add(' opacity: ' + IfThen(not Alpha.Present, 'none',
IfThen(Alpha.Premultiplied, 'premultiplied (CDEF typ 2)', 'raw (CDEF typ 1)')))
else
Log.Add(' opacity metadata unavailable (' + ErrorText + ')');
Log.Add(' codecs: ' + PdfImageComponentBackendVersions('PdfiumPasImageComponents.dll'));
end;
What turns transparent JPX areas into black boxes?
A lost alpha channel, which exposes color the encoder never meant anyone to see. ITU-T T.800 Annex I defines the Channel Definition box: each entry names a channel and gives it a type, 0 for color, 1 for opacity and 2 for premultiplied opacity, plus an association, where 0 means the whole image. With typ 2 the stored color is C × α, so every fully transparent pixel is stored as black. Drop the alpha and those pixels become an opaque black rectangle; skip the un-premultiply and antialiased edges darken into a fringe. A converter that ignores CDEF and counts four channels reads RGBA as CMYK, alpha lands in K, and every fully opaque pixel prints as 100% black ink. PDFium Component removes the opacity channel from the color set before choosing an interpretation, divides each color sample back by its alpha (zero alpha gives zero color) before any ICC transform, and writes the untouched alpha as an 8-bit DeviceGray /SMask. The order is forced by the math: an ICC transform is nonlinear, so converting C × α is not the same as converting C and scaling the result by α
/SMaskInData is the PDF's half of the same contract, read strictly. A value of 1 says the image data contains soft-mask values; ConvertPdfDocumentColours accepts it only when inspection really finds a CDEF opacity channel, since a flag promising alpha the stream lacks is malformed input that an all-opaque substitute mask would hide. A value of 2 says the color has been preblended with a background, and undoing that blend requires knowing the background, so such images are reported, as is an existing /SMask carrying /Matte. An explicit /ColorSpace overrides the JPX color declaration and has no say over opacity: over an RGBA codestream, the converter still reads the stream with the declared width, height and component count, finds the opacity channel and builds the same soft mask
Palette channels and subsampled alpha
Palette images add a level of indirection that is easy to collapse by accident. In a JP2 file with a PCLR palette, the CMAP box maps codestream components, directly or through palette columns, into channels, and CDEF describes those channels:
- With CMAP present, a CDEF number is a channel number, and the order of CDEF entries carries no meaning. Reading entry i as codestream component i, or as "the i-th accepted entry", pairs the wrong plane with the alpha once a palette or a permuted list is involved. When the alpha comes from a palette column, the component sample is an index checked against that component's own precision and signedness, and the column value is normalized by the column's precision
- Many encoders default four-component JP2 files to the CMYK enumeration (12). When CDEF marks the fourth channel as opacity, three color channels remain, the CMYK label no longer fits and is skipped, and the image takes another declaration or lands in the device fallback, the case
AllowJpxDeviceFallbackandJpxDeviceFallbackImagesare there for
Subsampled alpha follows the color planes. JPEG 2000 lets an opacity channel sit at a coarser sampling interval than the color, and the soft mask must land on the same reference grid as the image it cuts out. The converter gives each reference pixel the value of the source cell that contains it, the same rule it applies to subsampled color planes, and does not interpolate. A bilinear mask looks smoother at 2:1, but its edge stops coinciding with the color samples underneath, and at a hard cut-out the mismatch shows up as a thin line of background inside the object or object color outside it. Shrinking the converted image later raises other alpha questions, covered in the article on alpha-aware adaptive image resampling in Delphi
Run the first conversion of a new supplier's files with the device fallback disabled and read every reported object before relaxing anything; if gradients on the same pages shift too, the shading function conversion to CMYK goes through the same call in PDFium Component for Delphi