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.
- main.c: fix display_page(NULL) crash in webview mode by skipping display_page and calling webview_output_page directly - driver.c: update usage() to document -webview and -tmpdir flags - dvi_special.c: implement PDF v operator (first control point = current point) - dvi_special.c: implement PDF y operator (last control point = current point) - dvi_special.re2c.c: sync v/y operator changes to re2c source
- 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
Parse /Bounds and all sub-function C0/C1 data, then evaluate colors through the correct piecewise-linear stitching function instead of a single global lerp. This preserves solid-color endpoint regions and produces more vibrant, accurate gradients. Also increase steps to 500 with 50% overlap to eliminate visible banding.
When opacity < 1, strip overlap causes double-blending artifacts making the gradient darker than expected. Wrap the entire gradient in a MuPDF begin_group/end_group: render strips at full opacity, then composite the whole group at the correct alpha.
Rewrite render_radial_shade to use shade_eval_color for accurate color interpolation, 400 rings for smoothness, and transparency groups for correct alpha compositing.
Radial shadings use /Coords [x0 y0 r0 x1 y1 r1] (6 values), not the 4-value axial format. Previously only 4 floats were read, causing incorrect radius computation for elliptical gradients. Now parse up to 6 values and use r0/r1 directly from the PS.
…g function The stitching function with bound=0.5 compresses the transition into the inner 50% radius, making the outer color dominate 75% of the circle area and the center color disappear. Switch to simple full-range linear interpolation so the gradient transitions smoothly across the entire radius, matching the visual appearance of PDF-rendered radial shadings.
…stitching function" The full-range lerp diluted the center/edge colors even more. The stitching function preserves concentrated endpoint colors as specified by PGF's dvips driver. The color difference vs PDF is likely due to pdflatex using a different PGF driver with different shading parameters.
Parse pgfpat* pattern functions (crosshatch, horizontal lines, north east lines, dots) and render tiles directly via MuPDF device calls instead of relying on unsupported PostScript operators (makepattern, setcolor). Previously the custom PS interpreter silently ignored these operators, causing pattern fills to render as solid black.
…ndler - Don't set rendered=true for pgfpat* calls (they set state, don't draw) to prevent ps_exec_body/ps_code cleanup from dropping the rectangle path - Increase ps_func_def body buffer 256→512 to fit full pattern bodies (~350 chars) so PaintProc parsing doesn't fail on truncation - Add safety cap (500) on tile counts to avoid runaway loops
The function table was overflowing with ~50 PGF definitions (pgfsc, pgffc, pgf1-8, shading funcs, pgfpat*, etc.), causing pgffc and pgfpat* to be silently dropped. pgffc was never found by lookup, so all fills used the default black color regardless of pattern.
… 1 fills" This reverts the pattern rendering feature back to baseline. The PS_FUNC_MAX increase to 64 broke non-pattern fills. Will re-implement more carefully.
fz_clip_path call was causing segfault at address 8 (NULL dereference). Reverted to simpler approach without path clipping — works correctly for rectangular fills (the common case for Phase 13 patterns).
Part of the pattern rendering infrastructure — pattern_active init in dvi_context_begin_frame and pattern state fields in dvi_graphicstate.
…rn support" This reverts commit 61edc5f.
This reverts commit 18e47b0.
When pgffc contains a pgfpat reference, parse the color values directly from the body text instead of trying to execute the pattern's makepattern/setcolor operators (which are not implemented). This at least fills pattern rectangles with the correct color instead of black, avoiding the complexity and fragility of full pattern tiling.
Intercept pgffc body execution at definition time (not at pgffill time). When the body contains a pgfpat reference, parse the color directly instead of calling ps_exec_body which would silently ignore the pattern's makepattern/setcolor operators.
Store pgfpat pattern function bodies in a separate pats[] table (instead of ps_funcs) to avoid polluting the limited PS_FUNC_MAX table. This allows pattern lookup (pat_lookup) without expanding the main function table, preventing internal !-special functions from being spuriously executed by the token dispatcher. setup_pattern_from_pgffc looks up the pattern from pats[], parses PaintProc/BBox/XStep/YStep, and render_pattern_tiles clips to the fill path and tiles the PaintProc across the area.
Pattern texture rendering (render_pattern_tiles) still causes black screen / crash. Strip it back to the working color-only fix while keeping the pattern color display correct.
Remove compiled XDV/AUX/PDF/synctex files from tracking. Update .gitignore to cover *.aux, *.xdv globally, and test/out/.
Add TIKZ-SUPPORT.md (feature summary) and demo screenshots for pages 1, 3, and 5 of tikz-supported.tex.
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]; |
Copilot Review Follow-upAddressed the 15 Copilot review comments from this PR. Summary of changes: Fixed (13)
Deferred (2)
Commits4 commits on the |
|
Hi @yzr278892, |
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!