aboutsummaryrefslogtreecommitdiff
path: root/render.go
diff options
context:
space:
mode:
Diffstat (limited to 'render.go')
-rw-r--r--render.go163
1 files changed, 163 insertions, 0 deletions
diff --git a/render.go b/render.go
new file mode 100644
index 0000000..59330e5
--- /dev/null
+++ b/render.go
@@ -0,0 +1,163 @@
+package main
+
+import (
+ "fmt"
+ "image"
+ "math"
+
+ "github.com/gen2brain/go-fitz"
+)
+
+// outside is the cell color shown for the area around the page (margins).
+var outside = [3]uint8{30, 30, 30}
+
+// doc is an open PDF document for previewing.
+type doc struct {
+ f *fitz.Document
+
+ // Single-entry raster cache: cursor moves redraw every frame but only
+ // page/zoom/resize change the rendered bitmap, so avoid re-running MuPDF.
+ cachePage int
+ cacheDPI float64
+ cacheImg *image.RGBA
+}
+
+// raster returns the page rendered at dpi, reusing the cached bitmap when the
+// page and dpi are unchanged.
+func (d *doc) raster(pg int, dpi float64) (*image.RGBA, error) {
+ if d.cacheImg != nil && d.cachePage == pg && math.Abs(d.cacheDPI-dpi) < 1e-6 {
+ return d.cacheImg, nil
+ }
+ img, err := d.f.ImageDPI(pg, dpi)
+ if err != nil {
+ return nil, err
+ }
+ d.cachePage, d.cacheDPI, d.cacheImg = pg, dpi, img
+ return img, nil
+}
+
+// openDoc opens a PDF for rendering.
+func openDoc(path string) (*doc, error) {
+ f, err := fitz.New(path)
+ if err != nil {
+ return nil, err
+ }
+ return &doc{f: f}, nil
+}
+
+func (d *doc) close() error { return d.f.Close() }
+func (d *doc) numPage() int { return d.f.NumPage() }
+
+// pageSize returns the page dimensions in points (0-based page index).
+func (d *doc) pageSize(pg int) (w, h float64, err error) {
+ b, err := d.f.Bound(pg)
+ if err != nil {
+ return 0, 0, err
+ }
+ return float64(b.Dx()), float64(b.Dy()), nil
+}
+
+// cell is one terminal character drawn with an upper half-block: top is the
+// foreground (upper pixel), bot is the background (lower pixel).
+type cell struct {
+ top, bot [3]uint8
+}
+
+// view is a rendered page region mapped onto a terminal cell grid, plus the
+// affine mapping between cells and PDF points (bottom-left origin).
+type view struct {
+ cols, rows int
+ cells []cell // row-major, len rows*cols
+
+ scaleX, scaleY float64 // image px per pt
+ blankX, blankY int // pane-px offset where the image starts
+ scrollX, scrollY int // first visible image px
+ pageH float64
+}
+
+func (v *view) cell(r, c int) *cell { return &v.cells[r*v.cols+c] }
+
+// ptToCell maps a PDF point to the nearest cell (col,row).
+func (v *view) ptToCell(x, y float64) (col, row int) {
+ imgX := x * v.scaleX
+ imgY := (v.pageH - y) * v.scaleY
+ col = int(math.Round(imgX + float64(v.blankX-v.scrollX)))
+ row = int(math.Round((imgY + float64(v.blankY-v.scrollY)) / 2))
+ return col, row
+}
+
+// render rasterizes page pg (0-based) into a cols x rows cell grid. zoom is a
+// multiplier over fit-to-pane scale; the viewport is centered on (focusX,
+// focusY) in points when the rendered image is larger than the pane.
+func (d *doc) render(pg, cols, rows int, zoom, focusX, focusY float64) (*view, error) {
+ if cols < 1 || rows < 1 {
+ return nil, fmt.Errorf("pane too small")
+ }
+ pw, ph, err := d.pageSize(pg)
+ if err != nil {
+ return nil, err
+ }
+
+ paneX, paneY := cols, 2*rows
+ fit := math.Min(float64(paneX)/pw, float64(paneY)/ph)
+ scale := fit * zoom
+ dpi := scale * 72
+ if dpi < 2 {
+ dpi = 2
+ }
+
+ img, err := d.raster(pg, dpi)
+ if err != nil {
+ return nil, err
+ }
+ b := img.Bounds()
+ iw, ih := b.Dx(), b.Dy()
+ scaleX := float64(iw) / pw
+ scaleY := float64(ih) / ph
+
+ focusPxX := int(math.Round(focusX * scaleX))
+ focusPxY := int(math.Round((ph - focusY) * scaleY))
+ blankX, scrollX := axisMap(paneX, iw, focusPxX)
+ blankY, scrollY := axisMap(paneY, ih, focusPxY)
+
+ v := &view{
+ cols: cols, rows: rows, cells: make([]cell, cols*rows),
+ scaleX: scaleX, scaleY: scaleY,
+ blankX: blankX, blankY: blankY, scrollX: scrollX, scrollY: scrollY,
+ pageH: ph,
+ }
+
+ sample := func(paneXc, paneYc int) [3]uint8 {
+ ix := paneXc - blankX + scrollX
+ iy := paneYc - blankY + scrollY
+ if ix < 0 || ix >= iw || iy < 0 || iy >= ih {
+ return outside
+ }
+ c := img.RGBAAt(b.Min.X+ix, b.Min.Y+iy)
+ return [3]uint8{c.R, c.G, c.B}
+ }
+ for r := 0; r < rows; r++ {
+ for c := 0; c < cols; c++ {
+ cell := v.cell(r, c)
+ cell.top = sample(c, 2*r)
+ cell.bot = sample(c, 2*r+1)
+ }
+ }
+ return v, nil
+}
+
+// axisMap centers content within pane when it fits, otherwise scrolls so the
+// focus pixel is centered, clamped to content bounds.
+func axisMap(pane, content, focusPx int) (blank, scroll int) {
+ if content <= pane {
+ return (pane - content) / 2, 0
+ }
+ s := focusPx - pane/2
+ if s < 0 {
+ s = 0
+ }
+ if s > content-pane {
+ s = content - pane
+ }
+ return 0, s
+}