Repository navigation
Expand file tree
/
Copy pathpage.go
More file actions
372 lines (316 loc) · 11.1 KB
/
Copy pathpage.go
File metadata and controls
372 lines (316 loc) · 11.1 KB
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// Copyright (c) 2026 the go-pdfkit/pdfkit authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.
package pdfkit
import (
"bytes"
"math"
"strconv"
)
// Page is a single page's content. It accumulates a content stream as drawing
// methods are called; the operators mirror PDF's imaging model. The default
// user space has its origin at the lower-left corner with y increasing upward.
type Page struct {
doc *Document
width float64
height float64
buf bytes.Buffer
usedFonts map[*Font]bool
usedImages map[*imageXObject]bool
curFont *Font
curName string
fontSize float64
lineWidth float64
// text state, emitted inside each text object.
charSpace float64
wordSpace float64
leading float64
renderMode int
// lastTextState, lastFill and lastStroke are the operators last written for
// the text state, the fill colour and the stroke colour, so that setting
// the same value again writes nothing: all three are graphics-state
// parameters that persist across text objects, and a page of prose is
// thousands of words in one font and colour. Restore (Q) clears them,
// since it pops the state they were set in.
lastTextState string
lastFill string
lastStroke string
// extGStates records the transparency graphics states this page uses, in
// registration order; each becomes a /GS<i> resource.
extGStates []extGState
// shadings records the gradients this page paints, in registration order;
// each becomes a /Sh<i> resource. They are held per page and written into
// the resource dictionary directly, like the graphics states above: a
// shading is a small dictionary, not something worth an indirect object.
shadings []*pdfDict
// links are the clickable link annotations on this page, in the order added;
// each becomes a /Link annotation in the page's /Annots array.
links []linkAnnot
// dests are the named destinations anchored on this page; each becomes an entry
// in the document's /Dests name tree, jumped to by an internal GoTo link.
dests []namedDest
}
// linkAnnot is a clickable rectangle. When dest is empty it opens uri (an external
// URI action); otherwise it jumps to the named destination dest (a GoTo action).
type linkAnnot struct {
rect Rect
uri string
dest string
}
// namedDest is a named jump target anchored at (x, y) in the page's user space.
type namedDest struct {
name string
x, y float64
}
// AddLink adds a borderless clickable link over rect — in the same PDF user-space
// coordinates the drawing methods use — that opens uri when activated. It is how a
// typeset hyperref link (\href/\url) becomes navigable in the PDF, matching the
// <a href> the SVG output already emits.
func (p *Page) AddLink(rect Rect, uri string) {
p.links = append(p.links, linkAnnot{rect: rect, uri: uri})
}
// AddNamedDest anchors the named destination name at (x, y) on this page, so an
// internal link can jump to it. The point (x, y) is the top-left the viewer scrolls
// to, in the page's user space.
func (p *Page) AddNamedDest(name string, x, y float64) {
p.dests = append(p.dests, namedDest{name: name, x: x, y: y})
}
// AddNamedLink adds a borderless clickable link over rect that jumps to the named
// destination dest in the same document — the in-PDF counterpart of the SVG output's
// <a href="#name"> for \hyperlink.
func (p *Page) AddNamedLink(rect Rect, dest string) {
p.links = append(p.links, linkAnnot{rect: rect, dest: dest})
}
// Width returns the page width in points.
func (p *Page) Width() float64 { return p.width }
// Height returns the page height in points.
func (p *Page) Height() float64 { return p.height }
// op writes pre-formatted operands (already space-joined) followed by the
// operator and a newline.
func (p *Page) op(operands, operator string) {
if operands != "" {
p.buf.WriteString(operands)
p.buf.WriteByte(' ')
}
p.buf.WriteString(operator)
p.buf.WriteByte('\n')
}
// num formats a coordinate for the content stream.
func num(v float64) string { return ftoa(v) }
// nums joins several coordinates with spaces.
func nums(vs ...float64) string {
b := make([]byte, 0, 16)
for i, v := range vs {
if i > 0 {
b = append(b, ' ')
}
b = append(b, ftoa(v)...)
}
return string(b)
}
// Save pushes the current graphics state (q).
func (p *Page) Save() { p.op("", "q") }
// Restore pops the graphics state (Q). The font/colour operators written since
// the matching Save are forgotten with it, so the next SetFont/SetFillColor/
// SetStrokeColor writes even a value that looks unchanged.
func (p *Page) Restore() {
p.op("", "Q")
p.lastTextState, p.lastFill, p.lastStroke = "", "", ""
}
// Transform concatenates the affine matrix [a b c d e f] onto the current
// transformation matrix (cm). Points map as x' = a*x + c*y + e and
// y' = b*x + d*y + f.
func (p *Page) Transform(a, b, c, d, e, f float64) {
p.op(nums(a, b, c, d, e, f), "cm")
}
// Translate shifts the coordinate system by (tx, ty).
func (p *Page) Translate(tx, ty float64) { p.Transform(1, 0, 0, 1, tx, ty) }
// Scale scales the coordinate system by (sx, sy).
func (p *Page) Scale(sx, sy float64) { p.Transform(sx, 0, 0, sy, 0, 0) }
// Rotate rotates the coordinate system counter-clockwise by deg degrees about
// the origin.
func (p *Page) Rotate(deg float64) {
r := deg * math.Pi / 180
c, s := math.Cos(r), math.Sin(r)
p.Transform(c, s, -s, c, 0, 0)
}
// Skew shears the coordinate system by the given x and y angles in degrees.
func (p *Page) Skew(axDeg, ayDeg float64) {
p.Transform(1, math.Tan(ayDeg*math.Pi/180), math.Tan(axDeg*math.Pi/180), 1, 0, 0)
}
// MoveTo begins a new subpath at (x, y) (m).
func (p *Page) MoveTo(x, y float64) { p.op(nums(x, y), "m") }
// LineTo adds a straight segment to (x, y) (l).
func (p *Page) LineTo(x, y float64) { p.op(nums(x, y), "l") }
// CurveTo adds a cubic Bézier segment to (x3, y3) with control points
// (x1, y1) and (x2, y2) (c).
func (p *Page) CurveTo(x1, y1, x2, y2, x3, y3 float64) {
p.op(nums(x1, y1, x2, y2, x3, y3), "c")
}
// Rectangle adds a rectangle subpath (re).
func (p *Page) Rectangle(r Rect) {
p.op(nums(r.X, r.Y, r.Width, r.Height), "re")
}
// ClosePath closes the current subpath with a straight segment to its start
// (h).
func (p *Page) ClosePath() { p.op("", "h") }
// Stroke strokes the current path (S).
func (p *Page) Stroke() { p.op("", "S") }
// Fill fills the current path with the nonzero winding rule (f).
func (p *Page) Fill() { p.op("", "f") }
// FillEvenOdd fills the current path with the even-odd rule (f*).
func (p *Page) FillEvenOdd() { p.op("", "f*") }
// FillStroke fills (nonzero) then strokes the current path (B).
func (p *Page) FillStroke() { p.op("", "B") }
// FillStrokeEvenOdd fills (even-odd) then strokes the current path (B*).
func (p *Page) FillStrokeEvenOdd() { p.op("", "B*") }
// EndPath ends the path with no fill or stroke (n), used after a clip.
func (p *Page) EndPath() { p.op("", "n") }
// Clip intersects the clipping path with the current path using the nonzero
// rule (W). It must be followed by a path-painting or EndPath operator.
func (p *Page) Clip() { p.op("", "W") }
// ClipEvenOdd intersects the clipping path using the even-odd rule (W*).
func (p *Page) ClipEvenOdd() { p.op("", "W*") }
// SetLineWidth sets the stroke line width in user-space units (w).
func (p *Page) SetLineWidth(w float64) {
p.lineWidth = w
p.op(num(w), "w")
}
// Line-cap styles for SetLineCap.
const (
CapButt = 0
CapRound = 1
CapSquare = 2
)
// SetLineCap sets the line-cap style (J).
func (p *Page) SetLineCap(style int) { p.op(strconv.Itoa(style), "J") }
// Line-join styles for SetLineJoin.
const (
JoinMiter = 0
JoinRound = 1
JoinBevel = 2
)
// SetLineJoin sets the line-join style (j).
func (p *Page) SetLineJoin(style int) { p.op(strconv.Itoa(style), "j") }
// SetMiterLimit sets the miter limit (M).
func (p *Page) SetMiterLimit(limit float64) { p.op(num(limit), "M") }
// SetDash sets the line dash pattern and phase (d). An empty pattern restores a
// solid line.
func (p *Page) SetDash(pattern []float64, phase float64) {
var b bytes.Buffer
b.WriteByte('[')
for i, v := range pattern {
if i > 0 {
b.WriteByte(' ')
}
b.WriteString(ftoa(v))
}
b.WriteByte(']')
b.WriteByte(' ')
b.WriteString(ftoa(phase))
p.op(b.String(), "d")
}
// SetFillColor selects the fill colour. Selecting the colour already in force
// writes nothing.
func (p *Page) SetFillColor(c Color) {
if s := c.ops(false); s != p.lastFill {
p.op("", s)
p.lastFill = s
}
}
// SetStrokeColor selects the stroke colour. Selecting the colour already in
// force writes nothing.
func (p *Page) SetStrokeColor(c Color) {
if s := c.ops(true); s != p.lastStroke {
p.op("", s)
p.lastStroke = s
}
}
// extGState is one transparency graphics-state parameter dictionary this page
// references: a constant fill alpha (ca) and stroke alpha (CA).
type extGState struct {
fill float64
stroke float64
}
// SetAlpha sets the constant fill and stroke alpha (opacity) in [0,1] via an
// ExtGState resource (ca/CA).
func (p *Page) SetAlpha(fill, stroke float64) {
gs := extGState{fill: fill, stroke: stroke}
idx := -1
for i, g := range p.extGStates {
if g == gs {
idx = i
break
}
}
if idx < 0 {
idx = len(p.extGStates)
p.extGStates = append(p.extGStates, gs)
}
p.op("/GS"+strconv.Itoa(idx), "gs")
}
// finishContent returns the accumulated content-stream bytes.
func (p *Page) finishContent() []byte { return p.buf.Bytes() }
// resources builds the page's /Resources dictionary from the fonts and images
// it used and any transparency states it declared.
func (p *Page) resources(fontRefs map[*Font]objRef, imageRefs map[*imageXObject]objRef) *pdfDict {
res := newDict()
res.set("ProcSet", pdfArray{pdfName("PDF"), pdfName("Text"), pdfName("ImageB"), pdfName("ImageC"), pdfName("ImageI")})
if len(p.usedFonts) > 0 {
fonts := newDict()
for f := range p.usedFonts {
fonts.set("F"+strconv.Itoa(p.doc.fontIx[f]), fontRefs[f])
}
res.set("Font", sortDict(fonts))
}
if len(p.usedImages) > 0 {
xobj := newDict()
for _, x := range p.doc.images {
if p.usedImages[x] {
xobj.set(x.name, imageRefs[x])
}
}
res.set("XObject", xobj)
}
if len(p.extGStates) > 0 {
egs := newDict()
for i, gs := range p.extGStates {
d := newDict()
d.set("Type", pdfName("ExtGState"))
d.set("ca", pdfReal(gs.fill))
d.set("CA", pdfReal(gs.stroke))
egs.set("GS"+strconv.Itoa(i), d)
}
res.set("ExtGState", egs)
}
if len(p.shadings) > 0 {
sh := newDict()
for i, d := range p.shadings {
sh.set("Sh"+strconv.Itoa(i), d)
}
res.set("Shading", sh)
}
return res
}
// sortDict returns a copy of d with keys in ascending order, so a resource
// dictionary built from map iteration serialises deterministically.
func sortDict(d *pdfDict) *pdfDict {
type kv struct {
k string
v pdfValue
}
pairs := make([]kv, len(d.keys))
for i := range d.keys {
pairs[i] = kv{d.keys[i], d.vals[i]}
}
for i := 1; i < len(pairs); i++ {
for j := i; j > 0 && pairs[j-1].k > pairs[j].k; j-- {
pairs[j-1], pairs[j] = pairs[j], pairs[j-1]
}
}
out := newDict()
for _, p := range pairs {
out.set(p.k, p.v)
}
return out
}