readFullSvsMetadata function

Future<SvsFullMetadata?> readFullSvsMetadata(
  1. SvsFile svs
)

Reads all metadata from the SVS file, including all pyramid levels and associated images.

svs is the open SvsFile. Returns an SvsFullMetadata instance containing the levels and associated images, or null on failure.

Implementation

Future<SvsFullMetadata?> readFullSvsMetadata(SvsFile svs) async {
  if (svs is HamamatsuFile) return svs.fullMetadata();
  return await svs.synchronized(() async {
    final raf = svs.raf;
    final endian = svs.endian;
    final isBigTiff = svs.isBigTiff;
    int ifdOffset = svs.firstIfdOffset;

    List<SvsImageInfo> allImages = [];

    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;
      Map<String, String> properties = {};
      String? description;
      List<int> subIfdOffsets = [];

      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 == 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 == 270) {
          description = await _readTiffString(raf, entry.count, entry.valueOffset, inlineMaxBytes: entry.inlineMaxBytes, entryBd: entry.entryBd, offsetInEntry: entry.offsetInEntry);
          if (description != null) {
            properties = _parseAperioDescription(description);
          }
        } else if (entry.tag == 330) {
          subIfdOffsets = await _readTiffArray(raf, entry.dataType, entry.count, entry.valueOffset, endian, inlineMaxBytes: entry.inlineMaxBytes, entryBd: entry.entryBd, offsetInEntry: entry.offsetInEntry);
        }
      }

      String? imageType;
      if (tileWidth != null && tileHeight != null) {
        imageType = 'level';
      } else {
        imageType = _determineImageType(description, width, height, tileWidth, tileHeight) ?? 'other_association';
      }

      if (imageType == 'other_association') {
        if (width > 0 && height > 0) {
          double aspect = width / height;
          if (width < 2000 && height < 2000 && (aspect > 0.5 && aspect < 2.0)) {
            if (!allImages.any((img) => img.type == 'thumbnail')) {
              imageType = 'thumbnail';
            }
          }
        }
      }

      allImages.add(SvsImageInfo(
        type: imageType,
        width: width,
        height: height,
        tileWidth: tileWidth,
        tileHeight: tileHeight,
        compression: properties['Compression'],
        properties: properties,
        displayColor: parseDisplayColor(properties['DisplayColor']),
      ));

      // Process SubIFDs if present
      for (final subIfd in subIfdOffsets) {
        if (subIfd != 0) {
          final subHeader = await _readIfdHeader(raf, subIfd, endian, isBigTiff);
          if (subHeader != null) {
            int sWidth = 0;
            int sHeight = 0;
            int? sTileWidth;
            int? sTileHeight;
            Map<String, String> sProperties = {};
            String? sDescription;

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

              if (entry.tag == 256) {
                sWidth = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
              } else if (entry.tag == 257) {
                sHeight = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
              } else if (entry.tag == 322) {
                sTileWidth = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
              } else if (entry.tag == 323) {
                sTileHeight = _readTiffValue(entry.dataType, entry.count, entry.valueOffset, entry.entryBd, entry.offsetInEntry, endian);
              } else if (entry.tag == 270) {
                sDescription = await _readTiffString(raf, entry.count, entry.valueOffset, inlineMaxBytes: entry.inlineMaxBytes, entryBd: entry.entryBd, offsetInEntry: entry.offsetInEntry);
                if (sDescription != null) {
                  sProperties = _parseAperioDescription(sDescription);
                }
              }
            }

            String? sImageType;
            if (sTileWidth != null && sTileHeight != null) {
              sImageType = 'level';
            } else {
              sImageType = _determineImageType(sDescription, sWidth, sHeight, sTileWidth, sTileHeight) ?? 'other_association';
            }

            if (sImageType == 'other_association') {
              if (sWidth > 0 && sHeight > 0) {
                double aspect = sWidth / sHeight;
                if (sWidth < 2000 && sHeight < 2000 && (aspect > 0.5 && aspect < 2.0)) {
                  if (!allImages.any((img) => img.type == 'thumbnail')) {
                    sImageType = 'thumbnail';
                  }
                }
              }
            }

            allImages.add(SvsImageInfo(
              type: sImageType,
              width: sWidth,
              height: sHeight,
              tileWidth: sTileWidth,
              tileHeight: sTileHeight,
              compression: sProperties['Compression'] ?? properties['Compression'],
              properties: sProperties.isNotEmpty ? sProperties : properties,
              displayColor: parseDisplayColor(sProperties['DisplayColor'] ?? properties['DisplayColor']),
            ));
          }
        }
      }

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

    final levels = allImages.where((img) => img.type == 'level').toList();
    final Map<String, SvsImageInfo> associations = {};

    for (var img in allImages) {
      if (img.type != 'level') {
        String key = img.type ?? 'association';
        if (key == 'other_association') {
          key = 'assoc_${allImages.indexOf(img)}';
        } else {
          if (associations.containsKey(key)) {
            key = '${key}_${allImages.indexOf(img)}';
          }
        }
        associations[key] = img;
      }
    }

    levels.sort((a, b) => b.width.compareTo(a.width));

    final double baseWidth = levels.isNotEmpty ? levels.first.width.toDouble() : 1.0;
    final List<SvsImageInfo> levelsWithDownsample = levels.map((lvl) {
      final double downsample = (baseWidth > 0 && lvl.width > 0)
          ? (baseWidth / lvl.width)
          : 1.0;
      return lvl.copyWith(downsample: downsample);
    }).toList();

    return SvsFullMetadata(levels: levelsWithDownsample, associations: associations);
  });
}