Skip to content

fix(write): keep WebP images as WebP in client exports - #37

Merged
vlidholt merged 1 commit into
mainfrom
fix/webp-client-exports
Oct 26, 2026
Merged

vlidholt merged 1 commit into
mainfrom
fix/webp-client-exports

Conversation

@vlidholt

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #36. Images exported from Write as Markdown with images came out as .png files, and some had a large transparent area around the picture.

  • Cause: Write's client-side export re-encoded every image that wasn't PNG or JPEG into PNG with Flutter's Image.toByteData. Since feat(files): store uploaded images as WebP with Pixtronaut; drop BMP #36 every stored image is WebP, so every image took that path. On Flutter web (wasm), toByteData(format: png) resizes a shared offscreen canvas and then copies the whole canvas into the PNG, and those steps aren't atomic. When another image is rasterized in between, the PNG gets that canvas's size, with the image in its top-left corner. A reported file was 1627 × 2000 with the picture at 1:1 in the corner.
  • Fix: decodeForExport describes PNG/JPEG/GIF/WebP from their header with describeExportImage, as the server-side exports already did, and passes them through unchanged. Markdown (images/<n>-<slug>.webp) and HTML exports keep the WebP. Only other formats (SVG, AVIF of https images) still go through Flutter.
  • Office formats: DOCX/ODT/RTF/EPUB already convert in pure Dart (prepareOfficeImage). Opaque lossy WebP (photos) now becomes JPEG at quality 90 instead of a much larger PNG; lossless or transparent WebP becomes PNG. PDF decodes WebP itself, unchanged.
  • Sheets: Download image names the file by its sniffed type instead of assuming PNG for bytes from the server.
  • Blog submissions: checked, nothing to change. The webhook's assets[].type/name and the asset download's Content-Type come from the stored file (image/webp, *.webp), and submission_cover_test checks this. The webhook examples in docs/blog-integration.md and specs/18 now show a WebP asset.
  • Write spec 07 §4.4 updated.

This fix was first pushed to main by mistake (595119b) and reverted there (3a62134). This PR brings it back.

Testing

  • dart analyze and dart format clean.
  • New: export_service_test (a WebP passes the client resolver unchanged and is named .webp) and office_images_test (lossy opaque WebP → JPEG; lossless or transparent WebP → PNG).
  • wonky_write_core test/export (295), Write test/features/export and test/features/images, and the Sheets object/image command tests pass.
  • Not reproduced in a browser: the padding was diagnosed from the reported file and Flutter's skwasm source.

🤖 Generated with Claude Code

The client export resolver re-encoded every non-PNG/JPEG image as PNG
with Flutter's Image.toByteData. Now that stored images are WebP, every
image took that path, and on Flutter web (skwasm) the PNG can come back
on a larger transparent canvas when other images are rasterized at the
same time. Images are now described from their header like on the
server (describeExportImage): Markdown and HTML exports keep the WebP
(images/<n>-<slug>.webp), and the DOCX/ODT/RTF/EPUB exporters convert
opaque lossy WebP to JPEG (q 90) and other WebP to PNG in pure Dart.

Sheets' Download image names the file by the sniffed type instead of
assuming PNG for server bytes. The blog guide's and specs/18's webhook
examples show WebP assets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vlidholt
vlidholt merged commit dc175a6 into main Oct 26, 2026
41 checks passed
@vlidholt
vlidholt deleted the fix/webp-client-exports branch October 26, 2026 07:15
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant