1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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
}
|