exportDiagramPng function

Future<PngSnapshot> exportDiagramPng(
  1. DiagramPublication publication, {
  2. required PngOptions options,
  3. ImageProvider<Object> imageProvider(
    1. DiagramImageSource
    )?,
  4. DiagramTheme theme = DiagramTheme.light,
})

Renders a captured publication without a mounted canvas or document edit. Runtime overlays and path effects are not part of canonical document output. Content paints in its authored colors unless theme adapts them, as DiagramTheme.dark does.

Implementation

Future<PngSnapshot> exportDiagramPng(
  DiagramPublication publication, {
  required PngOptions options,
  ImageProvider<Object> Function(DiagramImageSource)? imageProvider,
  DiagramTheme theme = DiagramTheme.light,
}) async {
  final document = publication.state.document;
  final scene = publication.scene;
  if (scene.documentId != document.id || scene.revision != document.revision) {
    throw StateError('PNG output requires a revision-matched publication.');
  }
  final bounds = options.bounds;
  final ratio = options.pixelRatio;
  options.validate();
  final w = bounds.width * ratio;
  final h = bounds.height * ratio;
  // Bound allocations before converting untrusted floating-point dimensions.
  if (!w.isFinite ||
      !h.isFinite ||
      w <= 0 ||
      h <= 0 ||
      w > 8192 ||
      h > 8192 ||
      w.ceil() * h.ceil() > 16 * 1024 * 1024) {
    throw ArgumentError(
      'PNG output exceeds 8192 pixels per side or 16 megapixels.',
    );
  }
  final width = w.ceil(), height = h.ceil();
  final visible = scene.spatialIndex.query(bounds.inflate(80)).toList();
  final sources = <String, DiagramImageSource>{};
  for (final element in visible) {
    final stories = <ResolvedTextStory>[];
    if (element is ResolvedShape) {
      for (final source in element.images) {
        sources[source.uri] = source;
      }
      if (element.text case final text?) stories.add(text);
    } else if (element is ResolvedTextElement) {
      stories.add(element.text);
    } else if (element is ResolvedConnector) {
      stories.addAll(element.labels.map((label) => label.text));
    }
    for (final story in stories) {
      for (final line in story.lines) {
        if (line.geometry is! FlutterDiagramTextLine ||
            (line.resolvedMarker != null &&
                line.resolvedMarker!.geometry is! FlutterDiagramTextLine)) {
          throw StateError(
            'PNG output requires captured Flutter text geometry for ${element.id}.',
          );
        }
      }
    }
  }
  final images = DiagramImageCache(
    provider: imageProvider,
    maximumConcurrentLoads: options.resourceConcurrency,
    maximumDecodedBytes: options.maximumDecodedImageBytes,
    loadTimeout: options.resourceTimeout,
  );
  final cache = DiagramScenePaintCache();
  final camera = DiagramCamera.fixed(
    DiagramCameraState(
      center: bounds.center,
      zoom: 1,
      viewportSize: DiagramSize(bounds.width, bounds.height),
    ),
  );
  ui.Picture? picture;
  try {
    await loadOutputResources(
      sources.values,
      images.load,
      concurrency: options.resourceConcurrency,
      timeout: options.resourceTimeout,
    );
    final recorder = ui.PictureRecorder();
    final canvas = ui.Canvas(recorder);
    try {
      if (options.background case final background?) {
        canvas.drawColor(background.toColor(), ui.BlendMode.src);
      }
      canvas.scale(ratio);
      canvas.clipRect(Rect.fromLTWH(0, 0, bounds.width, bounds.height));
      DiagramScenePainter(
        scene: scene,
        camera: camera,
        selection: const EmptySelection(),
        grid: publication.state.options.grid,
        lodPolicy: const DiagramLodPolicy(),
        paintCache: cache,
        imageCache: images,
        contentOnly: true,
        theme: theme,
      ).paint(canvas, Size(bounds.width, bounds.height));
    } finally {
      picture = recorder.endRecording();
    }
    final bytes = await encodeDiagramPngPicture(picture, width, height);
    return PngSnapshot(
      bytes: bytes,
      documentId: document.id,
      revision: document.revision,
      textLayoutRevision: scene.textLayoutRevision,
      bounds: bounds,
      width: width,
      height: height,
    );
  } finally {
    picture?.dispose();
    cache.dispose();
    images.dispose();
  }
}