software-mansion / software-mansion/react-native-enriched-html

<img> wider than its rendering column fails to paint on Android — silently in isolation, with an "invalid measurement" error alongside any other text (not a decode-timing race)

Open
#797 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
C
Stars
1.4k
Forks
66
Avg merge
4d 19h
Merged PRs (30d)
11

Description

Describes the bug

An <img> tag whose declared width exceeds the actual width of the column EnrichedText renders it into fails to paint on Android. This is fully deterministic — not a timing race — and reproduces on the very first cold launch, every time.

I originally filed this issue theorizing an unsynchronized bounds race in AsyncDrawable (ImageDecoder.setTargetSize reading a still-zero Rect before the text-layout pass assigns real bounds). That was a plausible read of the source, but after extensive testing (cold cache, warm cache, a local low-latency image server, and the exact resolve-then-remount sequence a real consuming app uses, run concurrently across 16 images) I was never able to reproduce it. Everything I've actually observed — including the original production bug that led me to file this — is fully explained by the mechanism below instead, with no timing element at all. Retracting that theory.

The failure takes two different shapes depending on context:

  1. The <img> is the only content in the EnrichedText — the failure is completely silent. No exception, no broken-image placeholder, nothing logged anywhere.
  2. There's any other text in the same EnrichedText (true of essentially all real admin-authored content — headings, paragraphs, etc. around an image) — the same failure now logs a concrete diagnostic:
    ReactNativeJNI: EnrichedTextView returned an invalid measurement. Min: [0,0] Max: [384,inf] Actual: [400,404.333]
    
    Max is the real available column width; Actual is what the measure function reported back, driven by the embedded image's declared size.

This split traces to MeasurementStore.measure(): single-line content reports StaticLayout.getLineWidth(0), but multi-line content reports StaticLayout.width instead — and that can still exceed the nominal wrap width when one line contains an unsplittable inline image wider than the rest.

To Reproduce

Minimal repro app: https://github.com/ekrapfl/rne-html-repro-android-img-paint

  1. npm install && npm run android (native dev build, Fabric).
  2. Three cases render: an image that fits (paints normally), the same image declared far wider than the screen with no other content (renders at full size, overflowing the screen edge uncropped), and that same oversized image with a paragraph before it (reserves no visible space, shows nothing).
  3. adb logcat | grep -i "invalid measurement" around the third case.

Expected behavior

An oversized declared width shouldn't cause total, silent render failure. At minimum, a diagnostic should surface consistently (right now it only does when there's other text in the same view); ideally the library would clamp/scale the image to the available column itself, the way a browser does with img { max-width: 100% }, rather than leaving every consumer to compute and inject a safe pixel width themselves.

Device tested

  • Pixel 10 Pro XL emulator (sdk_gphone16k_arm64), Android API 37
  • react-native-enriched-html 1.1.1, React Native 0.86.3, Expo ~57.0.23, Fabric (New Architecture)

Additional context

One honest caveat: the visual failure reproduces reliably in the minimal repro above, on a fresh cold install, every time. The exact JNI log line was confirmed repeatedly against a full production app under a matching layout structure, but hasn't fired in the standalone repro despite matching nesting depth, padding, and styling — something about the fuller app context matters there that I haven't isolated. The visual symptom (reserves no space, never paints) is the same failure either way.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with MeasurementStore.measure() and the Android image path involving AsyncDrawable and ImageDecoder.setTargetSize. Run the linked minimal repro with the three oversized-image cases, then compare single-line and multi-line measurement behavior and the invalid-measurement log. Done means oversized images no longer fail silently or disappear during rendering.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, react-native
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.