Skip to content

[Web/Chrome] Picture.toImage() produces transparent strips on right and top edges at large canvas sizes #182476

Closed

Description

@yesilcimenahmet

Steps to reproduce

  1. Extract the attached project zip
  2. Run flutter pub get
  3. Run flutter run -d chrome
  4. Press the "PNG Export" button
  5. Save and inspect the exported PNG

ZIP Source File:

export_test.zip

Expected results

The exported PNG should be a pixel-perfect copy of the canvas content with clean edges and no corruption.

Image

Actual results

The exported PNG has fully transparent (alpha=0) strips along the right and top edges. The drawn content appears squeezed/shifted into a smaller area within the full image dimensions.
At 4960×7016:

Top edge: transparent horizontal strip, full width (4960px), height 166px
Right edge: transparent vertical strip, full height (7016px), width 117px

Image

Observations

Where it fails:

Chrome 145.0.7632.76, on two separate machines with different GPUs

Where it works correctly:

Firefox 147.0.4
Windows Desktop build
Android

Size dependency:

At 4960×7016 -> bug present
At 2480×3508 (half) -> works correctly

Format independent:
Both ImageByteFormat.png and ImageByteFormat.rawRgba produce the same corruption. The issue is in toImage(), not in toByteData() encoding.

WebGL parameters (Chrome DevTools):

(() => {
  const c = document.createElement('canvas');
  const gl = c.getContext('webgl2') || c.getContext('webgl');
  if (!gl) { console.log('No WebGL'); return; }
  const gp = p => gl.getParameter(p);
  console.log('MAX_TEXTURE_SIZE:', gp(gl.MAX_TEXTURE_SIZE));
  console.log('MAX_RENDERBUFFER_SIZE:', gp(gl.MAX_RENDERBUFFER_SIZE));
  console.log('MAX_VIEWPORT_DIMS:', gp(gl.MAX_VIEWPORT_DIMS));
  console.log('devicePixelRatio:', window.devicePixelRatio);
})();
  • MAX_TEXTURE_SIZE: 16384
  • MAX_RENDERBUFFER_SIZE: 16384
  • MAX_VIEWPORT_DIMS: Int32Array(2) [32767, 32767]
  • devicePixelRatio: 1

All WebGL limits are well above the requested canvas size.

Ruled out causes:

  • WASM build vs JS build: bug present in both
  • Canvas size aligned to multiples of 32: no effect
  • Chrome ANGLE backend (D3D11, OpenGL, Vulkan, WARP): same on all
  • Disable GPU (--disable-gpu --disable-gpu-compositing): same corrupt output
  • Disable PWA / service worker: no effect
  • Different GPU (GTX 1060 3GB vs RTX 4070): bug present on both

Tested on

  • Flutter: 3.41.1
  • Dart: 3.11.0
  • Chrome: 145.0.7632.76
  • Firefox: 147.0.4
  • GPUs: NVIDIA GTX 1060 3GB, NVIDIA RTX 4070
  • OS: Windows 11
  • iOS: Not tested (no device available)

Code sample

Code sample
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'helpers/save_file_web.dart';

void main() => runApp(const MaterialApp(home: ExportTest()));

const int kW = 4960;
const int kH = 7016;

class ExportTest extends StatefulWidget {
  const ExportTest({super.key});
  @override
  State<ExportTest> createState() => _ExportTestState();
}

class _ExportTestState extends State<ExportTest> {
  ui.Image? _img;
  String _status = 'Loading...';

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    final data = await rootBundle.load('assets/test.png');
    final codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    final frame = await codec.getNextFrame();
    setState(() {
      _img = frame.image;
      _status = 'Ready';
    });
  }

  Future<void> _export() async {
    if (_img == null) return;
    setState(() => _status = 'Export...');

    final rec = ui.PictureRecorder();
    final canvas = Canvas(
      rec,
      Rect.fromLTWH(0, 0, kW.toDouble(), kH.toDouble()),
    );

    final src = Rect.fromLTWH(
      0,
      0,
      _img!.width.toDouble(),
      _img!.height.toDouble(),
    );
    final dst = Rect.fromLTWH(0, 0, kW.toDouble(), kH.toDouble());
    canvas.drawImageRect(_img!, src, dst, Paint());

    final picture = rec.endRecording();
    final image = await picture.toImage(kW, kH);
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);

    if (byteData == null) {
      setState(() => _status = 'Error: null');
      return;
    }

    final bytes = byteData.buffer.asUint8List();
    await FileSaveHelper.saveFile(bytes, 'image.png');
    setState(
      () =>
          _status = 'OK: ${(bytes.length / 1024 / 1024).toStringAsFixed(1)} MB',
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            if (_img != null)
              Flexible(
                child: AspectRatio(
                  aspectRatio: kW / kH,
                  child: CustomPaint(painter: _Painter(_img!)),
                ),
              ),
            const SizedBox(height: 16),
            Text(_status),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _img == null ? null : _export,
              child: const Text('PNG Export'),
            ),
          ],
        ),
      ),
    );
  }
}

class _Painter extends CustomPainter {
  final ui.Image img;
  _Painter(this.img);

  @override
  void paint(Canvas canvas, Size size) {
    final src = Rect.fromLTWH(
      0,
      0,
      img.width.toDouble(),
      img.height.toDouble(),
    );
    final dst = Rect.fromLTWH(0, 0, size.width, size.height);
    canvas.drawImageRect(img, src, dst, Paint());
  }

  @override
  bool shouldRepaint(covariant _Painter old) => old.img != img;
}

Screenshots or Video

No response

Logs

No response

Flutter Doctor output

Doctor output
[√] Flutter (Channel stable, 3.41.1, on Microsoft Windows [Version 10.0.26200.7840], locale tr-TR) [355ms]
    • Flutter version 3.41.1 on channel stable at D:\fluttersdk\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 582a0e7c55 (4 days ago), 2026-02-12 17:12:32 -0800
    • Engine revision 3452d735bd
    • Dart version 3.11.0
    • DevTools version 2.54.1
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android,
      enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging,
      enable-uiscene-migration

[√] Windows Version (11 Pro 64-bit, 25H2, 2009) [1.098ms]

[√] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [1.945ms]
    • Android SDK at C:\Users\yesil\AppData\Local\Android\sdk
    • Emulator version 36.4.9.0 (build_id 14788078) (CL:N/A)
    • Platform android-36, build-tools 36.1.0
    • Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.9+-14649483-b1163.86)
    • All Android licenses accepted.

[√] Chrome - develop for the web [83ms]
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.14.26) [82ms]
    • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
    • Visual Studio Community 2022 version 17.14.36930.0
    • Windows 10 SDK version 10.0.26100.0

[√] Connected device (3 available) [178ms]
    • Windows (desktop) • windows • windows-x64    • Microsoft Windows [Version 10.0.26200.7840]
    • Chrome (web)      • chrome  • web-javascript • Google Chrome 145.0.7632.76
    • Edge (web)        • edge    • web-javascript • Microsoft Edge 144.0.3719.115

[√] Network resources [546ms]
    • All expected network resources are available.

• No issues found!

Activity

  1. saurabh-mirajkar commented on Feb 17, 2026

    @saurabh-mirajkar
    Contributor

    @yesilcimenahmet Thanks for the report.

  2. added
    platform-webWeb applications specifically
    team-webOwned by Web platform team
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Feb 17, 2026
  3. yesilcimenahmet commented on Feb 17, 2026

    @yesilcimenahmet
    Author

    @saurabh-mirajkar
    This bug is blocking our production release. Our application relies on exporting high-resolution images at 4960×7016, and this issue makes it impossible to export correctly on Chrome, which is the primary browser for our users.

    Is there any estimate on when this might be picked up? This is a critical blocker for us.

  4. saurabh-mirajkar commented on Feb 18, 2026

    @saurabh-mirajkar
    Contributor

    @yesilcimenahmet Thanks for the heads-up on the production impact. I’ve already assigned the ticket to team-web. Please note that the team doesn't typically provide firm ETAs for fixes, however they will pick up the ticket soon. Thanks.

  5. added
    P2Important issues not at the top of the work list
    triaged-webTriaged by Web platform team
    on Feb 18, 2026
  6. stuartmorgan-g commented on Feb 24, 2026

    @stuartmorgan-g
    Contributor

    @saurabh-mirajkar You've marked this c: regression but I don't see any discussion of a version where this worked. What version did you test that did not have this problem?

  7. saurabh-mirajkar commented on Feb 24, 2026

    @saurabh-mirajkar
    Contributor

    @saurabh-mirajkar You've marked this c: regression but I don't see any discussion of a version where this worked. What version did you test that did not have this problem?

    @stuartmorgan-g My apologies, I previously tested this on Flutter 3.38.10 and Chrome 145 without issue. However, I can now confirm the regression in Flutter 3.41.1 using the same Chrome version.

  8. jason-simmons commented on Aug 12, 2026

    @jason-simmons
    Contributor

    Reproduced this in Chrome on macOS

    This changed during the canvas/surface refactoring in cbd8912

    Part of the output appears to be clipped when the canvas is resized to the large size used to produce the app's output image. Smaller sizes work as expected.

    The output looks correct if I change CkSurface.setSize to create a new canvas instead of resizing the existing canvas.
    (by replacing the _canvasProvider.resizeCanvas call with recreateContextForCanvas(_canvasProvider.acquireCanvas(_currentSize, onContextLost: onContextLost)))

    @harryterkelsen

  9. jason-simmons commented on Aug 12, 2026

    @jason-simmons
    Contributor

    When run using Skwasm, this app also demonstrates a race related to the changes in cbd8912

    On my macOS/Chrome setup with Skwasm, clicking the "PNG Export" button in the app produces a PNG with size 1290x1732 instead of the intended 4960x7016.

    This happens because the onscreen display and the offscreen bitmap are both rendering using the same Skwasm::Surface instance. The size of the Skwasm::Surface is being overwritten before the offscreen bitmap is rendered.

    The offscreen bitmap is created by SkwasmImage.toByteData, which is async and does an await surface.setSize followed by an await surface.rasterizeToImageBitmaps. Other tasks may run during those awaits.

    In particular, a Renderer.renderScene may be scheduled, which will trigger another setSize call that sets the surface size to the onscreen window size. So by the time SkwasmImage.toByteData rasterizes the picture, the surface's size has been changed, and the output bitmap's size does not match the desired size.

  10. added
    P1High-priority issues at the top of the work list
    and removed
    P2Important issues not at the top of the work list
    on Aug 12, 2026
  11. harryterkelsen commented on Oct 29, 2026

    @harryterkelsen
    Contributor

    Quick status update on the two root causes Jason identified above:

    The Skwasm shared-surface resize race between renderScene and offscreen rasterization was fixed on master in #194401, which made resize and render atomic in a single raster-worker message. I had an older draft PR isolating pictureToImageSurface (#192770) that is superseded by that change, so I'm closing it.

    For the CanvasKit clipping at large dimensions (where Chromium silently clamps gl.drawingBufferWidth / drawingBufferHeight during in-place OffscreenCanvas resize), I have a draft fix in #192769 that keeps fast in-place resizing normally and recreates the offscreen canvas when the drawing buffer is clamped. I'm rebasing that onto current master to get it ready for review, and will also check whether Skwasm still hits the same Chromium WebGL drawing-buffer clamp at 4960×7016.

  12. harryterkelsen commented on Nov 10, 2026

    @harryterkelsen
    Contributor

    Both underlying issues are now fixed on master:

    Closing this issue as fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

P1High-priority issues at the top of the work listc: regressionIt was better in the past than it is nowhas reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-webWeb applications specificallyteam-webOwned by Web platform teamtriaged-webTriaged by Web platform team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions