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.
Implementation
PdfImageBase? decodePdfImageBase(CosDocument cos, CosStream stream) {
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);
if (cmyk == null) return null;
final rgba = _toRgba(cos, dict, cmyk.samples, cmyk.width, cmyk.height, 8,
icc: _iccProfileFor(cos, dict));
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;
final rgba = _jpxToRgba(jpx);
if (rgba == null) return null;
// _jpxToRgba writes 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 rgba = isMask
? _stencilToRgba(cos, dict, data, width, height)
: _toRgba(cos, dict, data, width, height, bits,
icc: _iccProfileFor(cos, dict));
if (rgba == null) return null;
// An /ImageMask decodes to a stencil with real (0/255) alpha.
return PdfImageBase(rgba, width, height, opaque: !isMask && !colorKeyed);
}