Skip to content

feat(android): add HTML-to-Bitmap renderer#470

Merged
jkmassel merged 4 commits intotrunkfrom
jkmassel/pattern-thumbnails
Apr 29, 2026
Merged

feat(android): add HTML-to-Bitmap renderer#470
jkmassel merged 4 commits intotrunkfrom
jkmassel/pattern-thumbnails

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented Apr 23, 2026

Summary

Groundwork for rendering block patterns as thumbnails in the native Android inserter. Introduces HtmlToBitmapRenderer: an off-screen WebView that loads an HTML string and snapshots the laid-out content to a Bitmap via Canvas.draw(). Lays out at the viewport width in device pixels, awaits onPageFinished, polls document.images.every(i => i.complete) so async-loaded images aren't missing from the snapshot, queries document.documentElement.scrollHeight, re-lays out to content height, then draws into a scaled bitmap. Never upscales.

Mirrors the iOS HTMLWebViewRenderer MVP under ios/Sources/GutenbergKit/Sources/Views/HTMLPreview/. No consumer yet — the BlockInserterDialog wiring that turns patterns into previews lands separately.

Design notes

  • Single-use WebView. Each call creates and destroys its own WebView in a finally. Pooling is a deliberate follow-up — iOS pools because WKWebView allocation is expensive, but we want to see the simpler model exercised before adding a pool.
  • Software layer + post-layout settle. setLayerType(LAYER_TYPE_SOFTWARE) forces the WebView to raster onto a software canvas rather than a GPU texture (which WebView.draw() can't reach off-screen). After re-layout we delay(POST_LAYOUT_SETTLE_MS) because there's no parent-window invalidation cycle to drive a repaint.
  • No caching here. Two-layer (memory + disk) caching, WebView pooling, and consumer wiring all land in follow-ups so this PR stays focused on the renderer primitive.

Known gaps

  • No caching. Renders are recomputed every call. (Follow-up.)
  • No WebView pool. (Follow-up.)
  • No unit tests. Robolectric's shadow WebView doesn't actually render. Coverage comes from the androidTest/ instrumented tests, which run in CI via the :android: Test Android Library Instrumented step (ci(android): run Gutenberg-module instrumented tests #472).

Test plan

  • ./gradlew :Gutenberg:detekt :Gutenberg:lintDebug :Gutenberg:compileDebugKotlin :Gutenberg:testBUILD SUCCESSFUL
  • Instrumented tests pass locally on a Pixel 9 Pro XL (./gradlew :Gutenberg:connectedAndroidTest)
  • CI's :android: Test Android Library Instrumented step passes on this PR

Follow-ups

  • Add a caching layer (CachingHtmlToBitmapRenderer + BlobCache/FileBlobCache) on top of the renderer.
  • Wire pattern thumbnails into BlockInserterDialog so the renderer is exercised end-to-end.

@github-actions github-actions Bot added the [Type] Enhancement A suggestion for improvement. label Apr 23, 2026
@jkmassel jkmassel force-pushed the jkmassel/pattern-thumbnails branch from e9b6fb7 to feb1c4f Compare April 24, 2026 16:43
@jkmassel jkmassel force-pushed the jkmassel/android-block-picker branch from 88d07a8 to 932e7ed Compare April 24, 2026 16:48
@jkmassel jkmassel force-pushed the jkmassel/pattern-thumbnails branch from feb1c4f to 9642d40 Compare April 24, 2026 16:51
@jkmassel jkmassel force-pushed the jkmassel/pattern-thumbnails branch from 9642d40 to bcf5343 Compare April 24, 2026 16:59
@jkmassel jkmassel changed the base branch from jkmassel/android-block-picker to trunk April 24, 2026 16:59
@jkmassel jkmassel force-pushed the jkmassel/pattern-thumbnails branch 2 times, most recently from 3b091fd to 3bfc76e Compare April 24, 2026 17:16
@jkmassel jkmassel force-pushed the jkmassel/pattern-thumbnails branch 3 times, most recently from f685d27 to 9a51157 Compare April 28, 2026 18:50
@jkmassel jkmassel changed the title feat(android): add HTML-to-Bitmap renderer with two-layer cache feat(android): add HTML-to-Bitmap renderer Apr 28, 2026
Adds HtmlToBitmapRenderer, an off-screen WebView wrapper that loads an
HTML string and snapshots the laid-out content to a Bitmap. Mirrors the
iOS HTMLWebViewRenderer MVP: no caching, no pooling — each call creates
and destroys its own WebView. A follow-up will layer caching and
WebView reuse on top.

All WebView interaction is marshalled onto the main thread internally,
so callers can invoke the suspending API from any dispatcher.
After onPageFinished, poll Array.from(document.images).every(i => i.complete)
at 50ms intervals up to a 4s soft timeout. Catches the common case where
<img> tags with srcset, lazy decoding, or async DOM insertion are still in
flight when window.load fires. On timeout, proceed with whatever has loaded
rather than fail the render — a partial thumbnail is better than none.

Same race exists on iOS; identical JS would close it there.
Adds JUnit instrumented tests that exercise the renderer against a real
Android WebView on an emulator or device — the unit-test layer can't
stand in for this because Robolectric's WebView shadow doesn't raster.

Runs in CI via the `:android: Test Android Library Instrumented` step
on the `mac-metal` queue. Each test writes its rendered PNG under the
app's external cache dir and logs the absolute path under the
`GBKRendererTest` tag so failures can be inspected with `adb pull` +
an image viewer.
Without a parent window, Chromium-backed WebViews render into a GPU texture
that WebView.draw(canvas) can't reach, producing blank bitmaps for CSS-only
content. Force LAYER_TYPE_SOFTWARE and add a short post-layout settle so the
software layer actually paints before pixels are read.
@jkmassel jkmassel force-pushed the jkmassel/pattern-thumbnails branch from 9a51157 to fc4fe18 Compare April 28, 2026 18:53
@jkmassel jkmassel marked this pull request as ready for review April 28, 2026 19:04
@jkmassel jkmassel requested a review from adalpari April 28, 2026 19:04
Copy link
Copy Markdown

@adalpari adalpari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 🚢 it!

@jkmassel jkmassel merged commit 6c2d3ea into trunk Apr 29, 2026
23 checks passed
@jkmassel jkmassel deleted the jkmassel/pattern-thumbnails branch April 29, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants