From 4b638dc6c46212ff3efffc1d746e72dd62502d9a Mon Sep 17 00:00:00 2001 From: Jenil D Gohel Date: Fri, 5 Dec 2025 17:36:01 +0530 Subject: [PATCH] Add size validation and warnings for Picture.toImage to prevent GPU texture limit issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change addresses issue #179432 where large SVG files (e.g., 5000x500 pixels) fail to render on Android devices due to GPU texture size limitations. Changes: 1. Added size validation in Picture.toImage() and Picture.toImageSync() - Hard limit at 8192 pixels (throws exception) - Warning threshold at 4096 pixels (prints warning but allows operation) 2. Added comprehensive error messages that suggest solutions: - Reducing SVG/Picture dimensions - Splitting into smaller segments - Using raster image formats instead 3. Added logging in C++ layer (picture.cc) to warn about oversized images 4. Added tests to verify the new validation behavior Background: Many Android devices have GPU texture size limits between 2048-4096 pixels, particularly mid-range and older devices. When vector_graphics attempts to rasterize large SVGs that exceed these limits, rendering fails silently or incompletely. This fix provides early detection and clear error messages. The 4096-pixel warning threshold was chosen based on common Android GPU capabilities, while the 8192-pixel hard limit prevents attempting rasterization that will almost certainly fail on most devices. Fixes #179432 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- engine/src/flutter/lib/ui/painting.dart | 56 ++++++++++++++ engine/src/flutter/lib/ui/painting/picture.cc | 11 +++ .../flutter/testing/dart/picture_test.dart | 74 +++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/engine/src/flutter/lib/ui/painting.dart b/engine/src/flutter/lib/ui/painting.dart index c952939b64c..6e9b703de65 100644 --- a/engine/src/flutter/lib/ui/painting.dart +++ b/engine/src/flutter/lib/ui/painting.dart @@ -7613,6 +7613,34 @@ base class _NativePicture extends NativeFieldWrapperClass1 implements Picture { if (width <= 0 || height <= 0) { throw Exception('Invalid image dimensions.'); } + // Check for dimensions that commonly exceed GPU texture limits. + // Many Android devices have max texture sizes between 2048-4096 pixels. + // While some modern GPUs support up to 8192 or 16384, we warn for + // dimensions above 4096 as they may fail on mid-range and older devices. + const int warningSizeThreshold = 4096; + const int hardLimitThreshold = 8192; + + if (width > hardLimitThreshold || height > hardLimitThreshold) { + throw Exception( + 'Image dimensions ($width x $height) exceed the maximum supported size ' + 'of $hardLimitThreshold pixels. The image cannot be rasterized.\n' + 'Consider:\n' + ' - Reducing the SVG/Picture dimensions\n' + ' - Splitting into smaller segments\n' + ' - Using a raster image format (PNG/JPEG) instead' + ); + } + + if (width > warningSizeThreshold || height > warningSizeThreshold) { + // Print warning but allow the operation to proceed + // ignore: avoid_print + print( + 'Warning: Picture.toImage dimensions ($width x $height) exceed $warningSizeThreshold pixels.\n' + 'This may fail on devices with limited GPU capabilities (particularly older Android devices).\n' + 'If rendering fails, consider reducing dimensions or splitting into smaller images.' + ); + } + return _futurize( (_Callback callback) => _toImage(width, height, (_Image? image) { if (image == null) { @@ -7638,6 +7666,34 @@ base class _NativePicture extends NativeFieldWrapperClass1 implements Picture { throw Exception('Invalid image dimensions.'); } + // Check for dimensions that commonly exceed GPU texture limits. + // Many Android devices have max texture sizes between 2048-4096 pixels. + // While some modern GPUs support up to 8192 or 16384, we warn for + // dimensions above 4096 as they may fail on mid-range and older devices. + const int warningSizeThreshold = 4096; + const int hardLimitThreshold = 8192; + + if (width > hardLimitThreshold || height > hardLimitThreshold) { + throw Exception( + 'Image dimensions ($width x $height) exceed the maximum supported size ' + 'of $hardLimitThreshold pixels. The image cannot be rasterized.\n' + 'Consider:\n' + ' - Reducing the SVG/Picture dimensions\n' + ' - Splitting into smaller segments\n' + ' - Using a raster image format (PNG/JPEG) instead' + ); + } + + if (width > warningSizeThreshold || height > warningSizeThreshold) { + // Print warning but allow the operation to proceed + // ignore: avoid_print + print( + 'Warning: Picture.toImageSync dimensions ($width x $height) exceed $warningSizeThreshold pixels.\n' + 'This may fail on devices with limited GPU capabilities (particularly older Android devices).\n' + 'If rendering fails, consider reducing dimensions or splitting into smaller images.' + ); + } + final image = _Image._(); _toImageSync(width, height, targetFormat.index, image); return Image._(image, image.width, image.height); diff --git a/engine/src/flutter/lib/ui/painting/picture.cc b/engine/src/flutter/lib/ui/painting/picture.cc index 4af953baed9..22108433d1a 100644 --- a/engine/src/flutter/lib/ui/painting/picture.cc +++ b/engine/src/flutter/lib/ui/painting/picture.cc @@ -172,6 +172,17 @@ Dart_Handle Picture::DoRasterizeToImage(const sk_sp& display_list, return tonic::ToDart("Image dimensions for scene were invalid."); } + // Warn about dimensions that may exceed common GPU texture limits. + // This provides a better error message than a silent failure or crash. + constexpr uint32_t kWarningSizeThreshold = 4096; + if (width > kWarningSizeThreshold || height > kWarningSizeThreshold) { + FML_LOG(WARNING) << "Picture.toImage dimensions (" << width << " x " + << height << ") exceed " << kWarningSizeThreshold + << " pixels. This may fail on devices with limited GPU " + << "capabilities. Consider reducing dimensions or " + << "splitting into smaller images."; + } + auto* dart_state = UIDartState::Current(); auto image_callback = std::make_unique( dart_state, raw_image_callback); diff --git a/engine/src/flutter/testing/dart/picture_test.dart b/engine/src/flutter/testing/dart/picture_test.dart index a15c0bb5a42..dfe1f6f8917 100644 --- a/engine/src/flutter/testing/dart/picture_test.dart +++ b/engine/src/flutter/testing/dart/picture_test.dart @@ -58,6 +58,80 @@ void main() { Picture.onDispose = null; }); + + test('toImage throws for dimensions exceeding hard limit', () async { + final picture = _createPicture(); + + // Test exceeding hard limit (8192 pixels) + expect( + () => picture.toImage(8193, 100), + throwsA(isA().having( + (e) => e.toString(), + 'message', + contains('exceed the maximum supported size'), + )), + ); + + expect( + () => picture.toImage(100, 8193), + throwsA(isA().having( + (e) => e.toString(), + 'message', + contains('exceed the maximum supported size'), + )), + ); + + picture.dispose(); + }); + + test('toImageSync throws for dimensions exceeding hard limit', () async { + final picture = _createPicture(); + + // Test exceeding hard limit (8192 pixels) + expect( + () => picture.toImageSync(8193, 100), + throwsA(isA().having( + (e) => e.toString(), + 'message', + contains('exceed the maximum supported size'), + )), + ); + + expect( + () => picture.toImageSync(100, 8193), + throwsA(isA().having( + (e) => e.toString(), + 'message', + contains('exceed the maximum supported size'), + )), + ); + + picture.dispose(); + }); + + test('toImage succeeds for normal dimensions', () async { + final picture = _createPicture(); + + // Should not throw for normal dimensions + final image = await picture.toImage(100, 100); + expect(image.width, 100); + expect(image.height, 100); + + image.dispose(); + picture.dispose(); + }); + + test('toImageSync succeeds for normal dimensions', () async { + final picture = _createPicture(); + + // Should not throw for normal dimensions + final image = picture.toImageSync(100, 100); + expect(image.width, 100); + expect(image.height, 100); + + image.dispose(); + picture.dispose(); + }); } Picture _createPicture() {