A pure-Go (no cgo) parser for TeX's SyncTeX
output — the .synctex.gz file pdflatex/xelatex/lualatex emit to link a
.tex source line to the exact spot on the PDF page, in both directions.
It answers the two queries an editor/viewer needs:
- Forward —
(source file, line)→(page, x, y): jump the PDF viewer to where a source line was typeset. - Backward —
(page, x, y)→(source file, line): click a glyph in the PDF and jump back to the source that produced it.
Zero external dependencies — standard library only. It cross-compiles and embeds
anywhere, is hardened against hostile input (a decompression-size cap guards
against zip-bomb .synctex.gz files), and can scope Input: paths to a project
root so absolute host paths never leak back to a client.
go get github.com/go-synctex/synctexpackage main
import (
"fmt"
"github.com/go-synctex/synctex"
)
func main() {
// Parse the .synctex.gz the TeX engine wrote next to the PDF.
f, err := synctex.Parse("main.synctex.gz")
if err != nil {
panic(err)
}
// Forward: source → PDF. "main.tex" line 42 lands here:
if r, ok := f.Forward("main.tex", 42); ok {
fmt.Printf("page %d at (%d, %d) sp\n", r.Page, r.X, r.Y)
}
// Backward: PDF → source. A click at (x, y) sp on page 2:
if src, line, ok := f.Backward(2, 32_000_000, 45_000_000); ok {
fmt.Printf("%s:%d\n", src, line)
}
}Coordinates are SyncTeX scaled points (sp); PDF points are sp / 65536. The
raw sp are exposed so callers can apply their own DPI/point scaling (PDF.js, for
instance, wants PDF points).
Input: entries in a .synctex.gz are absolute paths under the compile
workdir. When the file is untrusted or its paths would be echoed to a client,
pass WithProjectRoot — paths outside the root are dropped and paths inside are
rewritten relative-to-root:
f, err := synctex.Parse("build/main.synctex.gz",
synctex.WithProjectRoot("/srv/projects/thesis"))// Parse reads a .synctex.gz (or plain .synctex) file from disk.
func Parse(path string, opts ...Option) (*File, error)
// WithProjectRoot scopes Input: paths to root: outside paths are dropped,
// inside paths are rewritten relative-to-root.
func WithProjectRoot(root string) Option
// Forward: (source file, line) → nearest Record (page + x/y in sp).
func (f *File) Forward(sourceFile string, line int) (Record, bool)
// Backward: (page, x, y in sp) → (source file, line) closest to the point.
func (f *File) Backward(page, x, y int) (sourceFile string, line int, ok bool)
// ResolveSource maps an editor-side path to the absolute path SyncTeX recorded.
func (f *File) ResolveSource(rel string) (string, bool)
type Record struct {
FileTag int // Input: tag
Line int // 1-based source line
Page int // 1-based PDF page
X, Y int // anchor, in SyncTeX scaled points (sp)
}Pure standard-library logic — the same suite runs identically on every platform, so there is no interpreter or external oracle to install.
COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1 # 100.0%100% test coverage (including every error and malformed-record branch),
gofmt + go vet clean, and green across the six 64-bit Go targets — amd64,
arm64, riscv64, loong64, ppc64le, s390x.
BSD-3-Clause — see LICENSE. Copyright the go-synctex/synctex authors.
