decodePdfImageBase function

PdfImageBase? decodePdfImageBase(
  1. CosDocument cos,
  2. CosStream stream
)

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);
}