diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-22 17:43:57 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 9695cb098c784792af79c22d46a17f7bc7247d33 (patch) | |
| tree | fa446f3cb588b3184b7914b3f50c36cd3e5ece7b /src/pdf_bridge.c | |
| parent | f962de52df4b3d33438150920ae95b8708b0dde0 (diff) | |
| download | pardes-9695cb098c784792af79c22d46a17f7bc7247d33.tar.gz pardes-9695cb098c784792af79c22d46a17f7bc7247d33.zip | |
Repaint PDF highlights by row, send rasters by shared memory, and animate messages
PDF highlights (hover preview, search, selection) are baked into page rasters,
and any change re-rendered the whole page with MuPDF; the TTY then re-sent it
as base64 (4.7 MB a page), the GUI as a new texture. Worse, a pointer motion
over a PDF invalidated the page even when no preview was shown, so every
motion paid that. Now:
- A raster whose baked highlight set equals the wanted one is left alone.
- A highlighted page keeps its clean rows (before highlights and tint); a
change repaints only the rows of quads that differ, running MuPDF's
highlight pass (pardes_pdf_paint_highlights) over those clean rows and
tinting them: the operations a full render performs, so the pixels are
identical. MuPDF band renders are NOT bit-identical to a whole page (edge
rows, resampled images), so they are never used to patch; the comment
claiming otherwise is corrected.
- ImagePlace.patch hands shells the changed rows; the GUI uploads just those
rows into the texture it holds.
- The TTY probes kitty shared memory (t=s) with an id vaxis never reaches and
sends rasters as a /dev/shm name when the terminal reads it; direct base64
otherwise (ssh).
- Shells that take row patches (GUI, TTY with shm) repaint a selection while
it is dragged instead of only on release.
Latency elsewhere:
- TTY: an animating frame no longer sleeps 16 ms blind; a tick thread posts
into the input queue, so input inside the frame is handled at once.
- TTY and GUI: queued pointer motions coalesce to the last.
- GUI: a skipped swapchain image re-arms the frame (3 retries); animations
still tick while nothing presents.
- Editing: the line index is carried across an edit instead of rebuilt from
a scan of the whole file per keystroke.
Messages fall into their row (ease-in; the GUI slides the band out from under
the tagline, a terminal fades it), stay until the next input as before, linger
MessageLinger ms (default 800), and dissolve (ease-out). MessageAnimation
toggles it; both are settings, in Config and startup files. The snapshot
harness pins the old behaviour. The detached server now ticks animations.
A restored terminal comes back live: the old screen and scrollback (dumped
as clean VT by ghostty's formatter, replayed at the new size; older dumps
fall back to their rendered text), a dim
"restored history" marker, then a new shell in the directory it was in.
Right-click on a line number in a file pane looks at that line (a sticky
context header's number included).
Measured with an external pty driver (TTY), an in-process fence trace
(GUI, PARDES_TEST_LATENCY), and test/pdf_pointer_bench.zig (pixel identity
against the baseline and a whole-page oracle); balanced A/A/B rounds, paired
per-round statistics.
Messages stack: each event gets its own row and its own fall, linger and
dissolve; a line keeps its row until it leaves and a new one fills the first
free row. Announcements and statuses are replaced in place, not stacked.
MessageFall, MessageDissolve and DumpDir are settings Config reports.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to 'src/pdf_bridge.c')
| -rw-r--r-- | src/pdf_bridge.c | 228 |
1 files changed, 189 insertions, 39 deletions
diff --git a/src/pdf_bridge.c b/src/pdf_bridge.c index 976ed838..2c723dbb 100644 --- a/src/pdf_bridge.c +++ b/src/pdf_bridge.c @@ -572,6 +572,55 @@ pardes_pdf_measure_render( return PARDES_PDF_OK; } +/* The highlight pass of a render: fills over whatever the pixmap holds. */ +static void +pardes_pdf_fill_highlights(fz_context *ctx, pardes_pdf_document *document, + fz_device *device, const pardes_pdf_highlight *highlights, size_t highlight_count) +{ + fz_path *path = NULL; + size_t i; + + fz_var(path); + fz_try(ctx) + { + for (i = 0; i < highlight_count; ++i) { + fz_quad quad; + float color[3]; + float alpha; + + if (!pardes_pdf_valid_quad(highlights[i].quad)) + fz_throw(ctx, FZ_ERROR_ARGUMENT, + "PDF highlight contains a non-finite point"); + if (highlights[i].rgba[3] == 0) + continue; + + /* Kind is semantic metadata; caller-supplied RGBA is final. */ + (void)highlights[i].kind; + quad = pardes_pdf_quad_to_page(document, highlights[i].quad); + color[0] = highlights[i].rgba[0] / 255.0f; + color[1] = highlights[i].rgba[1] / 255.0f; + color[2] = highlights[i].rgba[2] / 255.0f; + alpha = highlights[i].rgba[3] / 255.0f; + + path = fz_new_path(ctx); + fz_moveto(ctx, path, quad.ul.x, quad.ul.y); + fz_lineto(ctx, path, quad.ur.x, quad.ur.y); + fz_lineto(ctx, path, quad.lr.x, quad.lr.y); + fz_lineto(ctx, path, quad.ll.x, quad.ll.y); + fz_closepath(ctx, path); + fz_fill_path(ctx, device, path, 0, fz_identity, + fz_device_rgb(ctx), color, alpha, + fz_default_color_params); + fz_drop_path(ctx, path); + path = NULL; + } + } + fz_always(ctx) + fz_drop_path(ctx, path); + fz_catch(ctx) + fz_rethrow(ctx); +} + int pardes_pdf_render_into( pardes_pdf_document *document, @@ -594,10 +643,8 @@ pardes_pdf_render_into( fz_context *ctx; fz_pixmap *pixmap = NULL; fz_device *device = NULL; - fz_path *path = NULL; fz_matrix ctm; fz_irect bbox; - size_t i; if (document == NULL || page_number < 0 || page_number >= document->page_count || dpi < 1 || @@ -614,7 +661,6 @@ pardes_pdf_render_into( ctx = document->ctx; fz_var(pixmap); fz_var(device); - fz_var(path); fz_try(ctx) { pardes_pdf_render_geometry(document, page_number, dpi, @@ -627,10 +673,12 @@ pardes_pdf_render_into( * The BAND: rows [band_y, band_y + band_height) of the page raster, * and nothing else. A pixmap's bbox IS the draw device's clip, so the * page runs under the same CTM it would for a full raster and MuPDF - * discards everything outside these rows. Every row inside them is - * therefore bit-identical to the same row of the full-page raster — - * "band rows equal full-page rows" in pdf.zig proves it, because the - * whole point of a band is that the reader cannot tell. + * discards everything outside these rows. The rows are NOT always + * bit-identical to the full page's: rows at the band's edges, and a + * resampled image anywhere in it, can differ (measured on + * docs/registry.pdf and docs/design.pdf). Good enough for a band that + * is only shown while flinging; never use a band to patch rows into a + * whole-page raster -- `pardes_pdf_paint_highlights` exists for that. */ bbox.y0 += band_y; bbox.y1 = bbox.y0 + band_height; @@ -674,43 +722,12 @@ pardes_pdf_render_into( if (highlight_count != 0) { device = fz_new_draw_device(ctx, ctm, pixmap); - for (i = 0; i < highlight_count; ++i) { - fz_quad quad; - float color[3]; - float alpha; - - if (!pardes_pdf_valid_quad(highlights[i].quad)) - fz_throw(ctx, FZ_ERROR_ARGUMENT, - "PDF highlight contains a non-finite point"); - if (highlights[i].rgba[3] == 0) - continue; - - /* Kind is semantic metadata; caller-supplied RGBA is final. */ - (void)highlights[i].kind; - quad = pardes_pdf_quad_to_page(document, highlights[i].quad); - color[0] = highlights[i].rgba[0] / 255.0f; - color[1] = highlights[i].rgba[1] / 255.0f; - color[2] = highlights[i].rgba[2] / 255.0f; - alpha = highlights[i].rgba[3] / 255.0f; - - path = fz_new_path(ctx); - fz_moveto(ctx, path, quad.ul.x, quad.ul.y); - fz_lineto(ctx, path, quad.ur.x, quad.ur.y); - fz_lineto(ctx, path, quad.lr.x, quad.lr.y); - fz_lineto(ctx, path, quad.ll.x, quad.ll.y); - fz_closepath(ctx, path); - fz_fill_path(ctx, device, path, 0, fz_identity, - fz_device_rgb(ctx), color, alpha, - fz_default_color_params); - fz_drop_path(ctx, path); - path = NULL; - } + pardes_pdf_fill_highlights(ctx, document, device, highlights, highlight_count); fz_close_device(ctx, device); } } fz_always(ctx) { - fz_drop_path(ctx, path); fz_drop_device(ctx, device); /* Drops only the wrapper: caller-owned samples remain untouched. */ fz_drop_pixmap(ctx, pixmap); @@ -743,6 +760,139 @@ pardes_pdf_render_into( } int +pardes_pdf_paint_highlights( + pardes_pdf_document *document, + int page_number, + int dpi, + int minimum_width, + int minimum_height, + int max_dimension, + const pardes_pdf_highlight *highlights, + size_t highlight_count, + unsigned char *samples, + size_t samples_len, + int width, + int height, + int stride, + int band_y, + int band_height) +{ + fz_context *ctx; + fz_pixmap *pixmap = NULL; + fz_device *device = NULL; + fz_matrix ctm; + fz_irect bbox; + + if (document == NULL || page_number < 0 || + page_number >= document->page_count || dpi < 1 || + minimum_width < 0 || minimum_height < 0 || max_dimension < 1 || + (highlight_count != 0 && highlights == NULL) || + highlight_count > PARDES_PDF_MAX_RESULT_QUADS || samples == NULL || + width < 1 || height < 1 || width > INT_MAX / 4 || stride != width * 4 || + band_y < 0 || band_height < 1 || band_y > height - band_height || + (size_t)band_height > SIZE_MAX / (size_t)stride || + samples_len != (size_t)stride * (size_t)band_height) + return PARDES_PDF_ERROR; + if (highlight_count == 0) + return PARDES_PDF_OK; + + ctx = document->ctx; + fz_var(pixmap); + fz_var(device); + fz_try(ctx) + { + pardes_pdf_render_geometry(document, page_number, dpi, + minimum_width, minimum_height, max_dimension, &ctm, &bbox); + if (fz_irect_width(bbox) != width || fz_irect_height(bbox) != height) + fz_throw(ctx, FZ_ERROR_ARGUMENT, + "PDF raster layout changed between measure and paint"); + bbox.y0 += band_y; + bbox.y1 = bbox.y0 + band_height; + pixmap = fz_new_pixmap_with_bbox_and_data( + ctx, fz_device_rgb(ctx), bbox, NULL, 1, samples); + device = fz_new_draw_device(ctx, ctm, pixmap); + pardes_pdf_fill_highlights(ctx, document, device, highlights, highlight_count); + fz_close_device(ctx, device); + } + fz_always(ctx) + { + fz_drop_device(ctx, device); + fz_drop_pixmap(ctx, pixmap); + } + fz_catch(ctx) + { + fz_report_error(ctx); + return PARDES_PDF_ERROR; + } + return PARDES_PDF_OK; +} + +int +pardes_pdf_highlight_rows( + pardes_pdf_document *document, + int page_number, + int dpi, + int minimum_width, + int minimum_height, + int max_dimension, + const pardes_pdf_highlight *highlights, + size_t highlight_count, + int *row_start, + int *row_end) +{ + fz_context *ctx; + fz_matrix ctm; + fz_irect bbox; + float y0 = 0, y1 = 0; + int any = 0; + size_t i; + + if (row_start == NULL || row_end == NULL) + return PARDES_PDF_ERROR; + *row_start = *row_end = 0; + if (document == NULL || page_number < 0 || + page_number >= document->page_count || dpi < 1 || + minimum_width < 0 || minimum_height < 0 || max_dimension < 1 || + (highlight_count != 0 && highlights == NULL)) + return PARDES_PDF_ERROR; + + ctx = document->ctx; + fz_try(ctx) + { + pardes_pdf_render_geometry(document, page_number, dpi, + minimum_width, minimum_height, max_dimension, &ctm, &bbox); + for (i = 0; i < highlight_count; ++i) { + fz_rect r; + if (highlights[i].rgba[3] == 0 || + !pardes_pdf_valid_quad(highlights[i].quad)) + continue; + r = fz_rect_from_quad(fz_transform_quad( + pardes_pdf_quad_to_page(document, highlights[i].quad), ctm)); + if (!any || r.y0 < y0) y0 = r.y0; + if (!any || r.y1 > y1) y1 = r.y1; + any = 1; + } + } + fz_catch(ctx) + { + fz_report_error(ctx); + return PARDES_PDF_ERROR; + } + if (any) { + /* + * Antialiasing covers the pixel a fractional edge falls in, and the + * rasterizer paints row floor(y1) even for a zero-height quad. + */ + int lo = (int)floorf(y0) - bbox.y0; + int hi = (int)floorf(y1) + 1 - bbox.y0; + int h = fz_irect_height(bbox); + *row_start = lo < 0 ? 0 : (lo > h ? h : lo); + *row_end = hi < *row_start ? *row_start : (hi > h ? h : hi); + } + return PARDES_PDF_OK; +} + +int pardes_pdf_page_text( pardes_pdf_document *document, int page_number, |
