Skip to content

Draw zoomed-in image pixels as sharp blocks - #75

Merged
PierreRaybaut merged 2 commits into
developfrom
fix/image-viewer-crisp-pixels
Oct 25, 2026
Merged

PierreRaybaut merged 2 commits into
developfrom
fix/image-viewer-crisp-pixels

Conversation

@PierreRaybaut

@PierreRaybaut PierreRaybaut commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #74

Summary

Since v0.6.1 (8d6f1f1), the uniform-image bitmap is a Plotly layout image, which the browser smooths whenever it is enlarged: zoomed-in pixels fade into each other. The former image trace avoided it (zsmooth: false → image-rendering: pixelated, or a magnified canvas on Safari and in exports), and DataLab desktop shows the nearest pixel by default.

  • Integer magnification of the bitmap (imageLod.ts, imageRaster.ts): when zoomed in (at least two device pixels per image pixel), the raster plan gets zoomX/zoomY factors (floor(display budget / visible pixels), within the 2048-pixel cap). The window is painted 1:1 as before, then copied onto the magnified canvas with imageSmoothingEnabled = false: every pixel is a sharp block, in every browser and in exported PNG images (modebar download, AI assistant capture). Small images are magnified whole when they fit, so panning never uncovers unpainted areas; otherwise only the visible window is painted, as for large images. The bitmap placement is unchanged (physical cell size).
  • CSS finishing touch (styles.css): image-rendering: pixelated (Plotly's declaration list) on the layout images of a view, set only while its bitmap shows undecimated pixels enlarged on screen (showsCrispPixels). The small remaining enlargement then stays sharp on Chrome, Edge and Firefox. Measured in Chromium: pixelated also samples to the nearest pixel when downscaling (a fine checkerboard turns black instead of mid-grey), so it is never set on reduced or decimated bitmaps.
    • image viewer (and the Create ROI grid… preview): from the measured plot size and view;
    • Spatial view: when every visible bitmap qualifies (typically when zoomed into one image);
    • processing preview (plot size not tracked): when the bitmap is magnified.
  • Always nearest pixel, like DataLab desktop's default; no new setting.
  • Changelog entry.

Not affected: thumbnails (already pixelated), non-uniform images and the profile dialog (Plotly heatmaps drawn at screen resolution).

Validation

  • vitest: 947 passed. New tests cover the magnification factors (DPR, two-pixel threshold, cap, independent axes), raster planning (whole/windowed/1:1), the magnified canvas (drawImage without smoothing, fallback without a 2D context), showsCrispPixels (magnified, slightly enlarged, reduced, decimated, DPR), and the CSS class in the image viewer and the spatial view.
  • Playwright (chromium): image-lod (including a new checkerboard test: integer magnification, a bitmap with exactly two colours, computed image-rendering: pixelated, and a screen row with only the two colours; the new test fails on the base branch), image-pan, image-roi-drag, multi-image-spatial: 10 passed.
  • Browser check on a zoomed 4×4 image: sharp pixels on screen and in a Plotly.toImage export.
  • tsc, eslint, prettier clean; npm run build within the bundle budget.
  • Raster cost measured in Chromium: magnifying and encoding a 2048² bitmap takes 20–55 ms (debounced 80 ms, as before).
  • Firefox and WebKit are not installed for Playwright here; the canvas magnification does not depend on the CSS, which only refines the result.

Since the uniform-image bitmap became a Plotly layout image (v0.6.1), the
browser smoothed it whenever it was enlarged: zoomed-in pixels faded into
each other, unlike DataLab desktop (nearest pixel by default) and unlike
the former image trace (pixelated style, magnified canvas on Safari and in
exports).

- the raster plan magnifies each painted cell by integer factors when
  zoomed in (from two device pixels per image pixel, within the bitmap
  size cap); the window is painted 1:1 as before, then copied onto the
  magnified canvas without smoothing, so pixels are sharp blocks in every
  browser and in exported PNG images; small images are magnified whole
  when they fit, so panning never uncovers unpainted areas;
- the views set image-rendering: pixelated on their layout images only
  while the bitmap shows undecimated pixels enlarged on screen, so the
  remaining enlargement stays sharp without aliasing reduced or decimated
  bitmaps (spatial view: when every visible bitmap qualifies; processing
  preview, whose plot size is not tracked: when the bitmap is magnified);
- the placement of the bitmap is unchanged (physical cell size).

Refs #74

Assisted-by: Claude Opus 5.5
@PierreRaybaut

Copy link
Copy Markdown
Contributor Author

CI does not trigger automatically on a PR stacked on another branch: the full \ ests\ workflow was dispatched on \ ix/image-viewer-crisp-pixels\ and passed (vitest, pytest 3.11/3.12, the 4 Playwright shards): https://github.com/DataLab-Platform/web/actions/runs/40203589859

@PierreRaybaut
PierreRaybaut deleted the branch fix/image-viewer-pixel-centers October 25, 2026 14:59
@PierreRaybaut PierreRaybaut reopened this Oct 25, 2026
@PierreRaybaut
PierreRaybaut changed the base branch from fix/image-viewer-pixel-centers to develop October 25, 2026 15:02
@PierreRaybaut PierreRaybaut reopened this Oct 25, 2026
@PierreRaybaut
PierreRaybaut merged commit 74f622b into develop Oct 25, 2026
19 checks passed
@PierreRaybaut
PierreRaybaut deleted the fix/image-viewer-crisp-pixels branch October 25, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image viewer: zoomed-in pixels are blurred instead of crisp (regression since v0.6.1)

1 participant