extractSvsImageAsImage function

Future<Image?> extractSvsImageAsImage(
  1. SvsFile svs,
  2. String type, {
  3. bool applyColorScheme = true,
  4. bool applyDisplayColor = true,
  5. int? displayColor,
})

Extracts an associated image (e.g. 'thumbnail', 'label', or 'macro') from an SVS file and returns it as a decoded img.Image.

Handles decompression (JPEG strips with JPEGTables, JPEG 2000, LZW with predictor, uncompressed RGB, Deflate) and stitches multi-strip images into a unified img.Image.

svs is the open SvsFile. type is the image type identifier to extract ('thumbnail', 'label', or 'macro'). applyColorScheme if true, applies the appropriate color scheme interpretation (e.g., treating RGB JPEG strips as RGB instead of default YCbCr, WhiteIsZero inversion, palette mapping). If false extracts the image as-is (backward compatible). applyDisplayColor if true, applies the DisplayColor color tinting/mapping if present in metadata or passed explicitly. displayColor optional explicit display color override (e.g., 0xRRGGBB). Returns the decoded img.Image, or null if the requested image type was not found or failed to decode.

Implementation

Future<img.Image?> extractSvsImageAsImage(
  SvsFile svs,
  String type, {
  bool applyColorScheme = true,
  bool applyDisplayColor = true,
  int? displayColor,
}) async {
  if (svs is HamamatsuFile) {
    svs.ensureOpen();
    try {
      final image = await svs.associations[type]?.readImageDecoded(
        applyColorScheme: applyColorScheme,
      );
      if (image != null && applyDisplayColor && displayColor != null && displayColor != 0) {
        return _applyDisplayColorToImage(image, displayColor);
      }
      return image;
    } catch (_) {
      return null;
    }
  }
  return await svs.synchronized(() async {
    final raf = svs.raf;
    final endian = svs.endian;
    final isBigTiff = svs.isBigTiff;
    int ifdOffset = svs.firstIfdOffset;
    Uint8List? globalJpegTables;

    while (ifdOffset != 0) {
      final header = await _readIfdHeader(raf, ifdOffset, endian, isBigTiff);
      if (header == null) break;

      int width = 0;
      int height = 0;
      int? tileWidth;
      int? tileHeight;
      int compression = 1;
      int samplesPerPixel = 3;
      int rowsPerStrip = 0;
      int predictor = 1;
      int photometricInterpretation = 2;
      String? description;
      List<int> stripOffsets = [];
      List<int> stripByteCounts = [];
      Uint8List? jpegTables;
      Uint8List? iccProfile;
      List<int>? colorMap;

      for (var i = 0; i < header.numEntries; i++) {
        final entry = await _readIfdEntry(raf, endian, isBigTiff);
        if (entry == null) break;

        if (entry.tag == 256) {
          width = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 257) {
          height = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 259) {
          compression = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 262) {
          photometricInterpretation = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 273) {
          stripOffsets = await _readTiffArray(raf, entry.dataType, entry.count, entry.valueOffset, endian, inlineMaxBytes: entry.inlineMaxBytes, entryBd: entry.entryBd, offsetInEntry: entry.offsetInEntry);
        } else if (entry.tag == 277) {
          samplesPerPixel = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 278) {
          rowsPerStrip = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 279) {
          stripByteCounts = await _readTiffArray(raf, entry.dataType, entry.count, entry.valueOffset, endian, inlineMaxBytes: entry.inlineMaxBytes, entryBd: entry.entryBd, offsetInEntry: entry.offsetInEntry);
        } else if (entry.tag == 317) {
          predictor = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 320) {
          colorMap = await _readTiffArray(raf, entry.dataType, entry.count, entry.valueOffset, endian, inlineMaxBytes: entry.inlineMaxBytes, entryBd: entry.entryBd, offsetInEntry: entry.offsetInEntry);
        } else if (entry.tag == 322) {
          tileWidth = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 323) {
          tileHeight = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
        } else if (entry.tag == 347) {
          final currentPos = await raf.position();
          await raf.setPosition(entry.valueOffset);
          jpegTables = await raf.read(entry.count);
          globalJpegTables ??= jpegTables;
          await raf.setPosition(currentPos);
        } else if (entry.tag == 34675) {
          final currentPos = await raf.position();
          await raf.setPosition(entry.valueOffset);
          iccProfile = await raf.read(entry.count);
          await raf.setPosition(currentPos);
        } else if (entry.tag == 270) {
          description = await _readTiffString(raf, entry.count, entry.valueOffset, inlineMaxBytes: entry.inlineMaxBytes, entryBd: entry.entryBd, offsetInEntry: entry.offsetInEntry);
        }
      }

      if (rowsPerStrip <= 0) {
        rowsPerStrip = height > 0 ? height : 1;
      }

      jpegTables ??= globalJpegTables;

      String? currentImageType = _determineImageType(description, width, height, tileWidth, tileHeight);

      if (currentImageType == type) {
        if (stripOffsets.isEmpty || stripByteCounts.isEmpty || width <= 0 || height <= 0) {
          return null;
        }

        int? ifdDisplayColor;
        if (description != null) {
          final props = _parseAperioDescription(description);
          ifdDisplayColor = parseDisplayColor(props['DisplayColor']);
        }
        final effectiveDisplayColor = displayColor ?? ifdDisplayColor;

        try {
          if (compression == 7 || compression == 6) {
            final fullImage = img.Image(width: width, height: height, numChannels: 3);
            for (int i = 0; i < stripOffsets.length; i++) {
              await raf.setPosition(stripOffsets[i]);
              var stripBytes = await raf.read(stripByteCounts[i]);
              if (jpegTables != null) {
                stripBytes = _combineJpegWithTables(stripBytes, jpegTables);
              }
              img.Image? stripImg;
              if (applyColorScheme && photometricInterpretation == 2) {
                try {
                  final adobeBytes = _injectAdobeMarker(stripBytes, 0);
                  stripImg = img.decodeJpg(adobeBytes);
                } catch (_) {
                  stripImg = null;
                }
              }
              if (stripImg == null) {
                try {
                  stripImg = img.decodeJpg(stripBytes);
                } catch (_) {
                  stripImg = null;
                }
              }
              if (stripImg == null) {
                try {
                  stripImg = img.decodeImage(stripBytes);
                } catch (_) {
                  stripImg = null;
                }
              }
              if (stripImg != null) {
                img.compositeImage(fullImage, stripImg, dstY: i * rowsPerStrip);
              }
            }
            if (applyColorScheme) {
              if (photometricInterpretation == 0) {
                _applyWhiteIsZero(fullImage);
              }
              if (iccProfile != null && iccProfile.length <= 65519) {
                fullImage.iccProfile = img.IccProfile('', img.IccProfileCompression.none, iccProfile);
              }
            }
            if (applyDisplayColor && effectiveDisplayColor != null && effectiveDisplayColor != 0) {
              return _applyDisplayColorToImage(fullImage, effectiveDisplayColor);
            }
            return fullImage;
          } else if (compression == 33003 || compression == 33005 || compression == 34712) {
            final fullImage = img.Image(width: width, height: height, numChannels: 4);
            for (int i = 0; i < stripOffsets.length; i++) {
              await raf.setPosition(stripOffsets[i]);
              var stripBytes = await raf.read(stripByteCounts[i]);
              img.Image? stripImg = _decodeJpeg2000(stripBytes);
              stripImg ??= img.decodeImage(stripBytes);
              if (stripImg != null) {
                img.compositeImage(fullImage, stripImg, dstY: i * rowsPerStrip);
              }
            }
            if (applyColorScheme) {
              if (photometricInterpretation == 0) {
                _applyWhiteIsZero(fullImage);
              }
              if (iccProfile != null && iccProfile.length <= 65519) {
                fullImage.iccProfile = img.IccProfile('', img.IccProfileCompression.none, iccProfile);
              }
            }
            if (applyDisplayColor && effectiveDisplayColor != null && effectiveDisplayColor != 0) {
              return _applyDisplayColorToImage(fullImage, effectiveDisplayColor);
            }
            return fullImage;
          } else if (compression == 5) {
            final totalBytes = width * height * samplesPerPixel;
            final uncompressedAll = Uint8List(totalBytes);
            int offset = 0;

            for (int i = 0; i < stripOffsets.length; i++) {
              await raf.setPosition(stripOffsets[i]);
              final compressedStrip = await raf.read(stripByteCounts[i]);

              int stripRows = rowsPerStrip;
              if ((i + 1) * rowsPerStrip > height) {
                stripRows = height - i * rowsPerStrip;
              }
              if (stripRows <= 0) break;

              int stripExpectedBytes = width * stripRows * samplesPerPixel;
              final decompressedStrip = _decompressTiffLzw(compressedStrip, stripExpectedBytes);

              if (predictor == 2) {
                _applyHorizontalPredictor(decompressedStrip, width, stripRows, samplesPerPixel);
              }

              if (offset + stripExpectedBytes <= uncompressedAll.length) {
                uncompressedAll.setRange(offset, offset + stripExpectedBytes, decompressedStrip);
              }
              offset += stripExpectedBytes;
            }

            img.Image? image = img.Image.fromBytes(
              width: width,
              height: height,
              bytes: uncompressedAll.buffer,
              numChannels: samplesPerPixel,
              order: samplesPerPixel >= 3 ? img.ChannelOrder.rgb : null,
            );
            if (applyColorScheme) {
              image = _applyColorSchemeToImage(
                image,
                photometricInterpretation,
                samplesPerPixel,
                colorMap,
                iccProfile,
                displayColor: (applyDisplayColor ? effectiveDisplayColor : null),
              );
            } else if (applyDisplayColor && effectiveDisplayColor != null && effectiveDisplayColor != 0) {
              image = _applyDisplayColorToImage(image, effectiveDisplayColor);
            }
            return image;
          } else if (compression == 1) {
            final bb = BytesBuilder();
            for (int i = 0; i < stripOffsets.length; i++) {
              await raf.setPosition(stripOffsets[i]);
              final bytes = await raf.read(stripByteCounts[i]);
              bb.add(bytes);
            }
            final rawBytes = bb.takeBytes();
            if (predictor == 2) {
              _applyHorizontalPredictor(rawBytes, width, height, samplesPerPixel);
            }
            img.Image? image = img.Image.fromBytes(
              width: width,
              height: height,
              bytes: rawBytes.buffer,
              numChannels: samplesPerPixel,
              order: samplesPerPixel >= 3 ? img.ChannelOrder.rgb : null,
            );
            if (applyColorScheme) {
              image = _applyColorSchemeToImage(
                image,
                photometricInterpretation,
                samplesPerPixel,
                colorMap,
                iccProfile,
                displayColor: (applyDisplayColor ? effectiveDisplayColor : null),
              );
            } else if (applyDisplayColor && effectiveDisplayColor != null && effectiveDisplayColor != 0) {
              image = _applyDisplayColorToImage(image, effectiveDisplayColor);
            }
            return image;
          } else if (compression == 8 || compression == 32946) {
            final totalBytes = width * height * samplesPerPixel;
            final uncompressedAll = Uint8List(totalBytes);
            int offset = 0;

            for (int i = 0; i < stripOffsets.length; i++) {
              await raf.setPosition(stripOffsets[i]);
              final compressedStrip = await raf.read(stripByteCounts[i]);

              int stripRows = rowsPerStrip;
              if ((i + 1) * rowsPerStrip > height) {
                stripRows = height - i * rowsPerStrip;
              }
              if (stripRows <= 0) break;

              int stripExpectedBytes = width * stripRows * samplesPerPixel;
              final decompressedStrip = Uint8List.fromList(zlib.decode(compressedStrip));

              if (predictor == 2) {
                _applyHorizontalPredictor(decompressedStrip, width, stripRows, samplesPerPixel);
              }

              if (offset + stripExpectedBytes <= uncompressedAll.length) {
                uncompressedAll.setRange(offset, offset + stripExpectedBytes, decompressedStrip);
              }
              offset += stripExpectedBytes;
            }

            img.Image? image = img.Image.fromBytes(
              width: width,
              height: height,
              bytes: uncompressedAll.buffer,
              numChannels: samplesPerPixel,
              order: samplesPerPixel >= 3 ? img.ChannelOrder.rgb : null,
            );
            if (applyColorScheme) {
              image = _applyColorSchemeToImage(
                image,
                photometricInterpretation,
                samplesPerPixel,
                colorMap,
                iccProfile,
                displayColor: (applyDisplayColor ? effectiveDisplayColor : null),
              );
            } else if (applyDisplayColor && effectiveDisplayColor != null && effectiveDisplayColor != 0) {
              image = _applyDisplayColorToImage(image, effectiveDisplayColor);
            }
            return image;
          } else {
            final bb = BytesBuilder();
            for (int i = 0; i < stripOffsets.length; i++) {
              await raf.setPosition(stripOffsets[i]);
              final bytes = await raf.read(stripByteCounts[i]);
              bb.add(bytes);
            }
            final rawBytes = bb.takeBytes();
            var image = img.decodeImage(rawBytes);
            image ??= _decodeJpeg2000(rawBytes);
            if (image != null) {
              if (applyColorScheme) {
                image = _applyColorSchemeToImage(
                  image,
                  photometricInterpretation,
                  samplesPerPixel,
                  colorMap,
                  iccProfile,
                  displayColor: (applyDisplayColor ? effectiveDisplayColor : null),
                );
              } else if (applyDisplayColor && effectiveDisplayColor != null && effectiveDisplayColor != 0) {
                image = _applyDisplayColorToImage(image, effectiveDisplayColor);
              }
            }
            return image;
          }
        } catch (e) {
          return null;
        }
      }

      ifdOffset = await _readNextIfdOffset(raf, header.nextIfdOffsetPos, endian, isBigTiff);
    }

    return null;
  });
}