decodePdfImageBase function

PdfImageBase? decodePdfImageBase(
  1. CosDocument cos,
  2. CosStream stream, {
  3. int? targetWidth,
  4. int? targetHeight,
})

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