ScreenshotNeo

BlogHow-to

How to Convert HTML to PNG in Flutter

Convert HTML to PNG in Flutter with native WebViews, control dimensions and timing, handle Flutter Web limits, and compare an API-based workflow.

By the ScreenshotNeo team29 September 202610 min read

How to Convert HTML to PNG in Flutter

To convert HTML to PNG in Flutter, render the HTML in a native WebView and capture the rendered result as PNG bytes. On Android and iOS, a dedicated package such as html_to_image_flutter or html_to_image is the shortest documented route. Both packages use Android WebView and iOS WKWebView and return image bytes that you can display with Image.memory, save to disk, upload, or share.

The implementation depends on your target. Mobile Flutter can use native WebViews directly. Flutter Web can embed HTML, but embedding is not the same as exporting a widget to PNG, so you need a browser-specific capture design and should verify it for your deployment. This guide covers both paths, sizing, delayed content, assets, platform differences, failure handling, and an API alternative.

1. Choose the right conversion path

Requirement Recommended path Reason
Android or iOS, HTML string or asset Dedicated converter package It renders through the platform WebView and returns PNG bytes.
Your app already depends on a WebView General WebView screenshot API You can control navigation and loading in the same WebView, but confirm whether capture is viewport-only.
Flutter Web Browser-specific capture workflow HtmlElementView embeds an HTML element; Flutter’s documentation does not present it as a complete portable PNG exporter.
Server-side or batch captures Screenshot API A remote browser avoids shipping native WebView setup and can process URLs asynchronously or in bulk.

For the mobile packages discussed here, the listed versions document Android SDK 21 and iOS deployment target 11.0 as minimums. Check the exact package version before changing your project settings because minimums and method signatures can change.

2. Add a dedicated HTML-to-image package

The package documentation for html_to_image_flutter exposes convertToImage for an HTML string and convertToImageFromAsset for a bundled HTML file. Its documented defaults include a 200 millisecond delay and a width based on the device width. The html_to_image package documents similar conversion methods plus paper sizes, custom dimensions, margins, layout strategies, delay, and device-scale-factor controls.

Add one package to pubspec.yaml. Pin a version after checking its current API documentation:

dependencies:
  flutter:
    sdk: flutter
  html_to_image_flutter: ^2.2.1

Run flutter pub get, then make sure your native project meets the package’s platform requirements. Keep conversion in a service class rather than inside a widget so that rendering, error reporting, and file handling remain testable.

3. Convert an HTML string to PNG bytes

This complete example follows the package’s documented intent: pass HTML to the converter, then show the returned Uint8List with Image.memory. Exact named arguments can vary by package release, so compare this structure with the API reference for the version in your lockfile.

HTML is rendered first; PNG encoding happens after the page reaches the required visual state.
HTML is rendered first; PNG encoding happens after the page reaches the required visual state.
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:html_to_image_flutter/html_to_image_flutter.dart';

class HtmlPngPage extends StatefulWidget {
  const HtmlPngPage({super.key});

  @override
  State<HtmlPngPage> createState() => _HtmlPngPageState();
}

class _HtmlPngPageState extends State<HtmlPngPage> {
  Uint8List? pngBytes;
  Object? error;
  bool loading = false;

  Future<void> convert() async {
    setState(() {
      loading = true;
      error = null;
    });

    const html = '''
      <!doctype html>
      <html>
        <head>
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <style>
            * { box-sizing: border-box; }
            body { margin: 0; padding: 24px; background: #f5f7fb;
                   font-family: Arial, sans-serif; color: #182230; }
            .card { width: 100%; padding: 24px; border-radius: 16px;
                    background: white; }
            h1 { margin: 0 0 8px; font-size: 28px; }
            p { margin: 0; line-height: 1.5; }
          </style>
        </head>
        <body>
          <section class="card">
            <h1>Invoice preview</h1>
            <p>Rendered from HTML inside a Flutter WebView.</p>
          </section>
        </body>
      </html>
    ''';

    try {
      final bytes = await HtmlToImageFlutter.convertToImage(
        html: html,
        delay: const Duration(milliseconds: 200),
      );
      if (!mounted) return;
      setState(() => pngBytes = bytes);
    } catch (e) {
      if (!mounted) return;
      setState(() => error = e);
    } finally {
      if (mounted) setState(() => loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('HTML to PNG')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            FilledButton(
              onPressed: loading ? null : convert,
              child: Text(loading ? 'Rendering…' : 'Convert'),
            ),
            if (error != null)
              Text('Conversion failed: $error',
                  style: const TextStyle(color: Colors.red)),
            if (pngBytes != null)
              Expanded(child: Image.memory(pngBytes!, fit: BoxFit.contain)),
          ],
        ),
      ),
    );
  }
}

If your installed package uses a top-level function instead of a class method, keep the same flow: await conversion, retain the returned Uint8List, and handle errors before updating the widget. Do not assume that creating an HTML widget automatically creates PNG bytes; conversion must be an explicit capture operation.

4. Convert a bundled HTML asset

Bundled templates are useful for invoices, certificates, reports, and offline output. Add the file to your Flutter assets:

flutter:
  assets:
    - assets/templates/invoice.html

Then call the package’s asset conversion method:

final Uint8List png = await HtmlToImageFlutter.convertToImageFromAsset(
  'assets/templates/invoice.html',
  delay: const Duration(milliseconds: 200),
);

Keep critical CSS inside the document. The package guidance recommends locally available images, fonts, and styles when deterministic offline rendering matters. Relative paths must resolve from the WebView’s asset context; test every image and font rather than assuming a browser URL will work inside the packaged app.

5. Control dimensions, paper size, margins, and scale

Output dimensions are controlled by both CSS layout and the converter’s capture settings. With html_to_image, the documented options include standard paper sizes, custom width and height, margins, layout strategies, delay, and device scale factor. Use custom dimensions when a downstream system expects a fixed pixel size; use a paper size when you are producing a document-like image.

Set the viewport explicitly in HTML and avoid unconstrained content:

<meta name='viewport' content='width=1200, initial-scale=1'>
<style>
  html, body { width: 1200px; margin: 0; }
  body { overflow: hidden; }
</style>

Android and iOS can measure the same CSS differently. The html_to_image documentation gives an A4 example of 595 × 842 pixels on Android and 1785 × 2526 pixels on an iPhone 16 Plus when a scale factor of 3 is involved. Those are package examples, not universal dimensions. If your output is unexpectedly large, inspect device-scale-factor behavior and whether physical pixels or logical CSS pixels are being requested.

For sharp text, raise scale carefully. A larger scale increases memory use and encoding time. For predictable results, choose one target width, set CSS dimensions explicitly, and record the package version and scale in your own output metadata.

6. Wait for JavaScript, images, and fonts

A WebView can finish its initial navigation before the page is visually complete. A fixed delay gives scripts and layout time to settle; the documented default for both dedicated packages is 200 milliseconds. That default is only a starting point.

  • Use a short delay for static inline HTML.
  • Increase the delay when JavaScript inserts content or remote images arrive later.
  • Prefer local assets for offline or repeatable captures.
  • Make images reserve space with explicit width and height to reduce layout shifts.
  • Wait for a known readiness condition if your package version supports it, rather than guessing with a very long delay.

For dynamic pages, add a marker after rendering:

<script>
  // Set this after data, images, and fonts are ready.
  window.document.documentElement.dataset.ready = 'true';
</script>

A package may not expose a selector wait, so you may need a conservative delay or a custom WebView implementation. Avoid network-dependent resources when the image must be identical across devices.

7. Saving, sharing, and uploading the PNG

The result is PNG bytes, so the rest is ordinary Flutter file handling. Write to an application directory with a path provider, or pass the bytes directly to a share or upload library. Avoid converting to a base64 string unless an API requires it; base64 increases payload size and memory pressure.

import 'dart:io';
import 'package:path_provider/path_provider.dart';

Future<File> savePng(Uint8List bytes, String name) async {
  final directory = await getApplicationDocumentsDirectory();
  final file = File('${directory.path}/$name.png');
  return file.writeAsBytes(bytes, flush: true);
}

For large captures, do not keep multiple full-resolution byte arrays in a list. Process one document, write or upload it, release the reference, and continue.

8. Using a general WebView screenshot API

If your application already uses flutter_inappwebview, a general WebView can be appropriate. Its documentation describes inline, headless, and in-app browser modes, and its screenshot API material describes PNG capture of the visible viewport. Confirm the current version’s method names, platform support, and whether you need the entire document or only what is visible. A viewport screenshot is not automatically a full-page export.

This route gives you more control over navigation, headers, JavaScript, and lifecycle, but it also makes readiness, sizing, and cleanup your responsibility. Use a headless or off-screen mode only after verifying that the target platform keeps the page laid out at the dimensions you request.

9. Flutter Web: embedding HTML is not exporting PNG

Flutter documents HtmlElementView as a way to place a browser HTML element in the widget tree. Its parent must provide bounded constraints, and embedding web content can be expensive. Flutter separately identifies webview_flutter as a plugin for embedding a full HTML page. These facilities solve embedding; the retrieved documentation does not establish a portable HTML-to-PNG export implementation.

For Flutter Web, decide where capture should happen:

  1. Capture in browser JavaScript using a browser-supported canvas or SVG pipeline, if your HTML is compatible and same-origin rules permit it.
  2. Send the HTML or a publicly reachable URL to a server that runs a real browser and returns PNG bytes.
  3. Use a service API when you need repeatable rendering across clients.

Test fonts, cross-origin images, CSS filters, and page height in the exact browser and hosting configuration. Do not promise that a mobile WebView package will work unchanged on Flutter Web.

10. Or skip the browser setup

If the HTML is available at a URL, ScreenshotNeo can return a PNG, JPEG, WebP, or PDF from one request. See the ScreenshotNeo API documentation for all options.

A clean capture removes consent and overlay elements before the final image is produced.
A clean capture removes consent and overlay elements before the final image is produced.
curl -G 'https://api.screenshotneo.com/v1/shot' \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    'https://api.screenshotneo.com/v1/shot',
    params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'},
    timeout=90,
)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the shot was billed. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan. Create a free ScreenshotNeo account.

11. Troubleshooting

Blank or transparent output

Cause: capture happened before layout, a WebView was not given bounds, or the page background is transparent. Fix: provide explicit width and height, add a readiness delay, set a body background, and verify that the WebView remains mounted until capture completes.

Images are missing

Cause: remote resources are unavailable offline, blocked by network policy, or still loading. Fix: bundle critical images, use resolvable paths, reserve image dimensions, and increase or condition the capture wait.

Text is clipped

Cause: the CSS width is larger than the capture width, overflow is hidden, or Android and iOS measured the layout differently. Fix: set an explicit viewport, inspect computed dimensions, remove accidental overflow, and test each platform.

Output is the wrong size

Cause: device scale factor or paper-size settings changed physical pixel dimensions. Fix: choose a known layout strategy, set width and height, and document the scale factor with the output.

Method or import errors

Cause: package APIs differ by version. Fix: open the versioned API reference, run flutter pub deps, and update the example to the installed signature instead of mixing documentation from another release.

Flutter Web works visually but cannot export

Cause: HtmlElementView embeds content but does not itself provide a PNG encoder. Fix: add a browser capture implementation or move rendering to a server/API.

12. Performance, reliability, and cost checklist

  • Keep HTML and critical CSS small; remove unused scripts before rendering.
  • Prefer local assets for offline and repeatable output.
  • Use explicit dimensions to reduce relayout and memory use.
  • Increase delays only for content that actually needs them.
  • Encode and upload one image at a time for batch jobs.
  • Test low-memory Android devices and older iOS hardware at your target scale.
  • For API workflows, use caching when the page can be reused and inspect billing/status headers so failed captures are distinguishable from successful shots.

Native conversion has no per-shot service fee, but it consumes device CPU, memory, storage, and network access. A remote API shifts browser maintenance and rendering capacity away from the app and is easier to centralize for scheduled or bulk captures. Choose based on where your HTML lives, whether capture must work offline, and whether identical server-side output matters.

13. FAQ

Can Flutter convert arbitrary HTML widgets directly?

No. A Flutter widget tree and an HTML document are different rendering systems. Supply HTML to a WebView-based converter or use a widget-specific image capture technique for Flutter widgets.

Does a 200 ms delay guarantee that images are ready?

No. It is the documented default for the cited packages, not a readiness guarantee. Dynamic pages may need a longer or condition-based wait.

Can I create a full-page PNG from a visible WebView screenshot?

Not automatically. Confirm whether your chosen WebView API captures only the visible viewport and use a package or server workflow that explicitly supports the required document size.

Why do Android and iOS produce different pixel counts?

CSS pixels, physical pixels, paper settings, and device scale factors can differ. Set dimensions and scale deliberately, then validate on both platforms.

When should I use ScreenshotNeo?

Use it when the source is reachable by URL, you want server-side browser rendering, or you need API, MCP, PDF, caching, bulk, or signed-link workflows without maintaining native WebViews.