Add TikZ/pgf rendering support - #135
Conversation
- New webview_output.h/c: render pages to QOI temp files, output JSON to stdout - renderer.h/c: expose txp_renderer_render_to_pixmap() and txp_renderer_invert_pixmap()
- synctex-backward: click-to-source reverse SyncTeX - set-page: jump to arbitrary page - set-output-size: request render resolution - go-home/go-end: jump to first/last page - reset-zoom: reset zoom level - set-fit-mode: set width/height/custom fit mode
- driver.h: add webview_mode, render_width/height, tmpdir to persistent_state - driver.c: -webview/-tmpdir flag parsing, conditional SDL init (TIMER+EVENTS) - main.c: webview mode guards, RELOAD_EVENT triggers webview_output_page, new command handlers (go-home, go-end, set-page, synctex-backward, etc.) - Makefile: add webview_output.o
- unhandled(): return 1 instead of 0, logging warnings instead of aborting the entire page DVI rendering - pdf_code catch: return 1 instead of 0 for unknown PDF operators This is the root cause of TikZ rendering failures: when any \special was unrecognized, dvi_exec_special returned false, which caused dvi_interp.c:383 to abort rendering the entire page. With this fix, unrecognized specials are merely skipped, allowing the rest of the page (including most TikZ graphics) to render correctly.
- change SDL window creation from SDL_WINDOW_SHOWN to SDL_WINDOW_HIDDEN to avoid potential hang during window creation in WSL2/WSLg - add explicit SDL_ShowWindow() call after renderer setup
- main.c: render page directly after engine advance in webview mode instead of relying on RELOAD_EVENT which may not fire in time - webview_output.c: fix mkstemp template (XXXXXX must be at end, not followed by .qoi) and add debug logging at each step - driver.c: use SDL_WINDOW_HIDDEN then SDL_ShowWindow to avoid potential blocking during window creation in WSL2/WSLg
- driver.h: add dark_mode field to persistent_state - webview_output.h/c: add dark_mode parameter, default to light mode - main.c: handle EDIT_INVERT by toggling dark_mode in webview mode, output dark-mode JSON for webview sync, trigger re-render on each engine advance (not just on page count change) for live editing
…, preloading - Fix zoom drift by using exact inverse zoom factors and recomputing offsets - Reduce scroll velocity and improve clamping to page bounds - Fix loading indicator i18n (updates with locale change) - Fix bidirectional SyncTeX precision (pass TeX points directly) - Fix reset zoom to properly reset resolution via set-output-size - Pass pageWidth/pageHeight from C backend for accurate coordinate mapping - Fix preview auto-close to check all open documents, not just visible editors - Fix page preloading for go-to-end and set-page (500 step advancement) - Renderer now scales content to fit pixmap dimensions
…ed page preloading - Revert SyncTeX backward to use scale_factor conversion (synctex_scan expects DVI units) - Implement dirty rect incremental rendering: compare old/new pixmaps, send only changed regions - Add time-based page preloading (up to 1s per go-to-end/set-page, max 5000 steps)
- Always render in webview mode after processing changes (not just when advance flag is set) - Fix dirty rect logic: skip sending when no changes detected (n_rects == 0) - Advance engine until it blocks for go-to-end and set-page (up to 20000 steps) - Send page-error message to webview when requested page doesn't exist
- Only render when had_changes || advance (avoid redundant renders) - Remove redundant render_page call for page dimension computation - Compute page dimensions only when auto-detecting render size - Add debug stderr logging for render attempts
Remove the inline webview render in the advance_engine block that caused each keystroke to trigger two MuPDF renders (one inline, one via RELOAD_EVENT). The RELOAD_EVENT handler is now the single render point. Also lower the auto-detect resolution multiplier from 3x to 2x to reduce data volume by ~55% for real-time editing feel.
Add inline render back for real-time editing feedback (no iteration delay), with a webview_rendered_this_iteration flag to prevent the RELOAD_EVENT handler from doing a redundant second render. This gives immediate visual feedback on keystrokes like the SDL window, while avoiding duplicate MuPDF rendering.
Modify need_advance() to keep advancing engine in webview mode until terminated, ensuring all pages are discovered on file open. Track steps_done in advance_engine() to prevent busy loops when engine is blocked. Limit inline render to content changes only (not during preload advancement). This enables the cascading go-end behavior: each click jumps to the last discovered page, and subsequent clicks discover more pages if the engine was previously blocked.
…w pages Change the event-loop continue condition in webview mode: only skip event handling (SDL_WaitEvent) when new pages were actually discovered (after_page_count > before_page_count). Otherwise fall through to process pending RELOAD_EVENT and other SDL events. This fixes the "stuck on loading" issue where the engine kept advancing without ever processing the initial RELOAD_EVENT.
Revert need_advance, advance_engine steps_done, and event-loop branching. Instead, add a simple preload loop before the main event loop that advances the engine until blocked or terminated. This avoids the event-scheduling complexity that caused the preview to get stuck. The preload runs synchronously before entering the main loop, so the initial RELOAD_EVENT is guaranteed to render page 0.
The engine's step() returns false when the TeX subprocess hasn't produced output yet (10us poll timeout) — this is normal during computation, not a signal that work is done. Changed preload, go-end, and set-page to retry instead of breaking, with a stalled counter (500k max) to prevent infinite loops if the engine hangs.
Remove the preload phase that ran synchronously before the main loop. Instead, rely on the existing lazy discovery: page 0 renders immediately, and go-end / set-page poll the engine to discover more pages on demand. This gives fast initial preview and avoids unnecessary resource usage for large documents.
500000 iterations (~5s at 10us/iter) blocks the UI thread for too long, causing navigation buttons to hang. 50000 (~500ms) is enough for the engine subprocess to produce output while keeping the UI responsive.
Output synctex-scroll message with TeX-point coordinates when forward SyncTeX finds a target, so the webview can pan to show the corresponding content.
Instead of hardcoding 2.5x in the C auto-detection, use a -resolution command-line flag that defaults to 2.5. This allows the extension to pass the user's configured defaultResolution setting.
TikZ commonly uses dash patterns with more than 4 elements (e.g., custom dashed lines with complex on/off sequences). The hard truncation at 4 elements silently broke these patterns in rendering.
Add support for BT, ET, Tf, Tj, TJ, Td, TD, Tm, T*, Tc, Tw, Tz, TL,
Tr, Ts, ', '' operators in the PDF content stream interpreter.
TikZ node labels (\node {text}) and decoration text now render.
- Add dvi_textstate to dvi_graphicstate for text matrix/font state
- Add font cache to dvi_context for resolving standard PDF fonts
- Implement show_special_text() using fz_show_glyph
- Text integrates with existing dvi_context_flush_text() mechanism
…, etc. Replace stderr "unhandled" warnings with silent no-ops for: - BMC, BDC, EMC (marked content - used by TikZ layers) - BX, EX (compatibility sections) - MP, DP (marked points) - ri, i (rendering intent, flatness) - d0, d1 (Type 3 font metrics) - CS, cs, SC, sc, SCN, scn (color space operators - TikZ uses direct color operators rg/RG/g/G/k/K instead) Remaining unhandled operators (gs, sh, Do) produce warnings and will be addressed in subsequent phases.
…rency Thread alpha values through all fz_fill_path, fz_stroke_path, and fz_fill_text calls. Previously these were hardcoded to 1.0 (fully opaque). The gs operator handler is a no-op for now; alpha values default to 1.0 and will be set once ExtGState resource parsing is implemented. - Add fill_alpha, stroke_alpha fields to dvi_graphicstate - Initialize to 1.0 in dvi_context_begin_frame() - Thread through all path painting and text rendering calls
…t document - Add re2c pattern for 'papersize=W,H' special format used by some packages - Add test/tikz-supported.tex covering all currently supported TikZ features: dash patterns (>4 elements), text nodes, paths, fills/strokes, line styles, colors, graphic state (q/Q/cm/W), clipping, transparency, decorations, shapes, and comprehensive flow charts
There was a problem hiding this comment.
Pull request overview
This PR expands TeXpresso’s rendering pipeline to handle TikZ/pgf output inside the DVI/XDV flow, while also adding a headless webview/QOI output path and new test fixtures to exercise the feature set. It fits into the codebase as a substantial extension of both the DVI-special interpreter and the frontend rendering/output layer.
Changes:
- Add TikZ-oriented PDF/PostScript special handling in the DVI renderer, covering paths, text, colors, clipping, opacity, shadings, and related graphics state.
- Add a webview/headless frontend path that renders pages to pixmaps/QOI files and exposes new editor commands for paging, sync, fit, output size, and dark mode.
- Add large regression/demo LaTeX fixtures plus documentation describing supported TikZ behavior and known limitations.
Reviewed changes
Copilot reviewed 18 out of 23 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
.gitignore |
Ignores additional generated TeX/XDV/test artifacts. |
doc/TIKZ-SUPPORT.md |
Documents supported TikZ features, architecture, and limitations. |
src/dvi/dvi_context.c |
Resets per-frame graphics and PS-related DVI state. |
src/dvi/dvi_prim.c |
Applies alpha to fills/text and couples DVI push/pop with graphics-state save/restore. |
src/dvi/dvi_special.re2c.c |
Adds the bulk of TikZ support in PDF/PS special parsing and rendering. |
src/dvi/mydvi.h |
Extends DVI state/context for alpha, text state, dash arrays, and font caching. |
src/frontend/Makefile |
Builds the new webview output module. |
src/frontend/driver.c |
Adds webview CLI flags, headless SDL init, and persistent webview settings. |
src/frontend/driver.h |
Stores webview/dark-mode/render-size state in the frontend driver. |
src/frontend/editor.c |
Parses new webview/navigation/sizing editor commands. |
src/frontend/editor.h |
Declares the new editor command variants and payloads. |
src/frontend/main.c |
Wires webview rendering/output, new commands, and sync/page handling into the main loop. |
src/frontend/renderer.c |
Exposes pixmap rendering/inversion helpers for headless output. |
src/frontend/renderer.h |
Declares the new pixmap helper APIs. |
src/frontend/webview_output.c |
Encodes rendered pages/dirty rects to QOI and emits webview protocol messages. |
src/frontend/webview_output.h |
Declares webview output entry points. |
test/minimal-text-test.tex |
Adds a minimal text-rendering fixture. |
test/tikz-shading-test.tex |
Adds a shading-focused TikZ/pgfplots fixture. |
test/tikz-supported.tex |
Adds a broad multi-phase TikZ feature exercise document. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (ps->webview_mode) | ||
| { | ||
| ui->doc_renderer = NULL; // No SDL renderer in webview mode | ||
| } | ||
| else | ||
| { | ||
| ui->doc_renderer = txp_renderer_new(ps->ctx, ui->sdl_renderer); | ||
| } |
| int row_min_x[4096]; // max page height ~4000 | ||
| int row_max_x[4096]; | ||
| int dirty_start = -1; | ||
|
|
||
| if (h > 4096) h = 4096; // safety |
| int rfd = mkstemp(rpath); | ||
| if (rfd >= 0) { | ||
| write(rfd, rqoi_data, rqoi_len); | ||
| close(rfd); | ||
| if (i > 0) fprintf(stdout, ","); |
| // Save current RGB for next incremental comparison | ||
| if (prev_rgb) free(prev_rgb); | ||
| prev_rgb = rgb; | ||
| prev_w = w; | ||
| prev_h = h; |
| ps->render_width = cmd.set_output_size.width; | ||
| ps->render_height = cmd.set_output_size.height; |
| void *qoi_data = qoi_encode(rgb, &desc, &qoi_len); | ||
| if (!qoi_data) { | ||
| fprintf(stderr, "[webview] ERROR: qoi_encode returned NULL\n"); | ||
| return; |
| st->registers_stack.base[st->registers_stack.depth] = st->registers; | ||
| // Save graphics state so that PS specials within TeX groups (e.g. | ||
| // \resizebox measurement passes) don't leak CTM / color changes to | ||
| // subsequent content. Reuses the same gs_stack as PS gsave/grestore | ||
| // because TeX groups and PS save/restore pairs are properly nested. | ||
| if (st->gs_stack.depth < st->gs_stack.limit) { | ||
| st->gs_stack.base[st->gs_stack.depth] = st->gs; | ||
| st->gs_stack.depth += 1; | ||
| } |
| // Advance text matrix horizontally | ||
| float tx = (adv * fs + st->gs.text.char_space) * hs + st->gs.text.word_space; |
| // TRM = CTM × Tm × Tfs: font_size scales the glyph only, | ||
| // not the page-level translation. | ||
| fz_matrix trm = fz_pre_scale(fz_identity, fs * hs, fs); // Tfs | ||
| trm = fz_concat(trm, st->gs.text.Tm); // Tfs × Tm | ||
| trm = fz_concat(trm, ctm); // (Tfs × Tm) × CTM |
| case PDF_OP_Tr: | ||
| { | ||
| float c[1]; | ||
| vstack_get_floats(ctx, stack, c, 1); | ||
| st->gs.text.render = (int)c[0]; |
- Fix vstack_get_floats consuming stack before vstack_get_arguments in dquote - Apply text rise (Ts) to TRM, previously parsed but ignored - Apply word_space only to space characters (U+0020), not every glyph - Respect text render mode (Tr) in dvi_context_flush_text - Handle SCN/scn with variable operand counts (Gray/RGB/CMYK/Pattern) - Fix gs_stack overflow silently corrupting save/restore pairing - Guard webview mode against NULL doc_renderer in theme/crop commands - Validate set-output-size dimensions (reject non-positive) - Clear path_out on qoi_encode failure to avoid uninitialized read - Fix JSON corruption and prev_rgb desync in webview diff output
Reverted: - Text render mode (Tr) handling in flush_text - Text rise (Ts) application in show_special_text - Word spacing only-for-spaces behavior - gs_stack overflow safety check in dvi_exec_push Kept: - dquote: get all operands at once to prevent vstack reset - SCN/scn: handle variable operand counts (Gray/RGB/CMYK/Pattern)
…nder mode, text rise, word_space) - let-def#12: refuse dvi_exec_push when gs_stack is full to prevent save/restore corruption - let-def#13: apply word_space only to space characters (U+0020) per PDF spec - let-def#14: apply text rise (Ts) to TRM in show_special_text - let-def#15: respect text render mode (Tr) in dvi_context_flush_text
- let-def#10: limit TRACE sp output to first 100 specials (was unconditional, flooding stderr with thousands of lines per TikZ page) - let-def#2: dynamically allocate row_min_x/row_max_x for pages taller than 4096px instead of silently clamping and missing bottom-region changes
Copilot Review Follow-upAddressed the 15 Copilot review comments from this PR. Summary of changes: Fixed (13)
Deferred (2)
Commits4 commits on the |
…bility The extension parser expects nRects at position 7 and the rects array at position 8. The previous change moved nRects after the array, breaking incremental/diff rendering in the built-in preview.
The main loop used 'if (advance) continue' which skipped all event processing during engine compilation. In webview mode, RELOAD_EVENT requests were starved until the engine stopped advancing. Now peek for pending custom events via SDL_PeepEvents before continuing. @tikz
|
Hi @yzr278892, |
The pattern color extraction code (commits 00e750e and 5f51065) was added directly to the generated dvi_special.c, not the re2c source. When re2c regenerated dvi_special.c during debug cleanup, the pattern color extraction was silently lost, causing pattern fills to render black instead of their specified colors. Move the pattern color extraction into dvi_special.re2c.c: - At pgffc definition time: parse RGB from pgfpat bodies - At pgffill: handle pgfpat-prefixed pgffc bodies - At pgfr/pgfR: same as pgffill
…esize - Change default_resolution from 2.5 to 3.0 for sharper rendering - Resolution now re-detects when preview window size changes >15% - Auto-detect formula uses 1.2x quality multiplier for ~3x baseline
Replace manual tile iteration loop with MuPDF native tiling API. - fz_begin_tile draws tile content once, MuPDF handles repetition - Eliminates complex tile bounds calculation and CTM-per-tile state - Adds forward declarations for pattern globals before pdf_code - Fixes compilation with re2c regeneration
- Reset active_pattern and in_pattern_tile in ps_state_reset() to prevent cross-page state leakage - Add debug fprintf in pdf_pat_define, pdf_pat_map_name, and render_pdf_pattern_fill for runtime diagnostics - Pattern definitions (pdf_pats[]) preserved across pages
- B* stroke used fill colors instead of stroke colors (copy-paste bug) - b/b*/B/B* fill-and-stroke operators now support pattern fills - f/F/f* reset active_pattern.active after rendering to prevent stale pattern state leaking to subsequent fills
The single-pass bounding box loop recomputed rw against a shrinking rx, so when later rows had smaller min_x, earlier rows' max_x+1-rx was never re-evaluated with the final rx. This left right-edge dirty pixels outside the rect, causing stale content (ghost traces) to persist until a full page re-render on page flip. Fixed by splitting into two passes: first compute global min rx, then compute rw with the final rx value.
# Conflicts: # src/dvi/dvi_special.c # src/dvi/dvi_special.re2c.c
Delete webview-specific files (webview_output.c/h, test-webview-test.tex, ghost-test.tex, doc screenshots, PR summary). Restore shared files to upstream/main versions (no tikz code was in them): - main.c, driver.c, driver.h, editor.c, editor.h, renderer.c, renderer.h - Makefile, EDITOR-PROTOCOL.md, .gitignore, engine_tex.c, state.h - tectonic_provider.c, zlib_md5.c Tikz branch now contains only tikz-related code: dvi_special.c/re2c.c, dvi_prim.c, dvi_context.c, mydvi.h, tikz test docs.
What this PR does
Adds TikZ/pgf rendering support to TeXpresso. A built-in PostScript interpreter and PDF content-stream handler process the DVI specials emitted by PGF, covering most common TikZ features.
What's supported
ill,\draw,illdraw)Demo
The test document
test/tikz-supported.texexercises all supported features across 18 test phases and 9 pages. Below are screenshots of pages 1, 3, and 5 rendered by TeXpresso in VSCode:Known limitations
texpresso file.xdv) hangs after page 1 (VSCode extension pipeline is fine)\matrixuses a manual node grid instead of the&syntax (ampersand conflicts with the frontend)Implementation notes
Two driver backends are handled: dvips (
ps:specials, stack-machine interpreter) and dvipdfmx (pdf:specials, vstack-based PDF operator interpreter). Shadings are rendered natively through MuPDF. A detailed feature summary is indoc/TIKZ-SUPPORT.md.Thank you for reviewing!