decodePdfImageBase function
Decodes the base image samples of stream to straight-alpha RGBA, WITHOUT
applying any /SMask or stencil /Mask. Returns null for a non-CMYK DCTDecode
base (needs the platform codec) or an undecodable image.
CMYK JPEGs decode here (the component decode is pure, via package:image),
as do Flate/raw DeviceRGB/Gray/Indexed, CCITT, JBIG2, JPX, and /ImageMask
stencils. Splitting the base out lets the dart:ui layer pair a purely
decoded base with a DCT-encoded soft mask (e.g. a CMYK image under a JPEG
/SMask) without re-decoding the base.
targetWidth/targetHeight may request a smaller base. For the expensive
8-bit Separation/DeviceN and CMYK paths, component samples are reduced
before the tint/ICC/device conversion, so a masked page image does not
allocate and colour-convert a native RGBA surface merely to shrink it one
layer up. Other formats retain their established full-resolution decode.
Implementation
PdfImageBase? decodePdfImageBase(
CosDocument cos,
CosStream stream, {
int? targetWidth,
int? targetHeight,
}) {
final dict = stream.dictionary;
final filters = pdfImageFilters(cos, dict);
final isMask = cos.resolve(dict['ImageMask']) == const CosBoolean(true);
// A color-key /Mask (an array of sample ranges, §8.9.6.4) is the only thing
// besides an /SMask or stencil /Mask that can turn a base pixel transparent.
final colorKeyed = cos.resolve(dict['Mask']) is CosArray;
final dctName = filters.contains('DCTDecode')
? 'DCTDecode'
: filters.contains('DCT')
? 'DCT'
: null;
if (!isMask && dctName != null) {
// undo any wrapping filters (e.g. [/FlateDecode /DCTDecode])
final jpeg = cos.decodeStreamData(stream, stopBeforeFilter: dctName);
if (pdfImageColorFamily(cos, dict) != 'DeviceCMYK') {
return null; // non-CMYK JPEG → platform codec
}
final cmyk = _decodeDctCmyk(
jpeg,
targetWidth: targetWidth,
targetHeight: targetHeight,
);
if (cmyk == null) return null;
final colorT0 = PdfPerf.begin();
final rgba = _toRgba(
cos,
dict,
cmyk.samples,
cmyk.width,
cmyk.height,
8,
icc: _iccProfileFor(cos, dict),
);
PdfPerf.end(PdfPerfPhase.imageColorConvert, colorT0);
if (rgba == null) return null;
return PdfImageBase(rgba, cmyk.width, cmyk.height, opaque: !colorKeyed);
}
if (filters.contains('JPXDecode')) {
final jpx = JpxDecoder.decode(
cos.decodeStreamData(stream, stopBeforeFilter: 'JPXDecode'));
if (jpx == null) return null;
// An /Indexed JPX carries palette indices in its single component, not
// colour: the samples must run through the lookup table, exactly as a raw
// /Indexed image does. Treating them as gray paints the raw index value -
// for a single-entry palette (hival 0) that is index 0 → solid black,
// which is how GWG170/GWG172 rendered a black square over the "no X must
// be visible" marker (issue #431).
final rgba = pdfImageColorFamily(cos, dict) == 'Indexed'
? _jpxIndexedToRgba(cos, dict, jpx)
: _jpxToRgba(jpx);
if (rgba == null) return null;
// The mappers write alpha 255 throughout (JPX carries no color key).
return PdfImageBase(rgba, jpx.width, jpx.height, opaque: true);
}
// CCITTFaxDecode runs as a regular stream filter (pure-Dart decoder in
// pdf_cos) and lands here as 1-bit gray samples.
final width = _intOf(cos.resolve(dict['Width']));
final height = _intOf(cos.resolve(dict['Height']));
if (width <= 0 || height <= 0) return null;
final bits = _intOf(cos.resolve(dict['BitsPerComponent']), fallback: 8);
final Uint8List data;
if (filters.contains('JBIG2Decode')) {
final decoded = Jbig2Decoder.decode(
data: cos.decodeStreamData(stream, stopBeforeFilter: 'JBIG2Decode'),
globals: _jbig2Globals(cos, dict),
width: width,
height: height,
);
if (decoded == null) return null;
data = decoded;
} else {
data = cos.decodeStreamData(stream);
}
final family = pdfImageColorFamily(cos, dict);
final alternate = _alternateColorSpaceFor(cos, dict);
final components = alternate?.channels ??
switch (family) {
'DeviceCMYK' => 4,
_ => 0,
};
final shouldScale = targetWidth != null &&
targetHeight != null &&
!isMask &&
!colorKeyed &&
bits == 8 &&
components > 0 &&
(family == 'Separation' || family == 'DeviceN' || family == 'DeviceCMYK');
final colorT0 = PdfPerf.begin();
final converted = shouldScale
? _convertAndDownsampleRgba(
cos,
dict,
data,
width,
height,
bits,
components,
targetWidth,
targetHeight,
)
: null;
final rgba = converted?.rgba ??
(isMask
? _stencilToRgba(cos, dict, data, width, height)
: _toRgba(cos, dict, data, width, height, bits,
icc: _iccProfileFor(cos, dict)));
PdfPerf.end(PdfPerfPhase.imageColorConvert, colorT0);
if (rgba == null) return null;
// An /ImageMask decodes to a stencil with real (0/255) alpha.
return PdfImageBase(
rgba, converted?.width ?? width, converted?.height ?? height,
opaque: !isMask && !colorKeyed);
}