Repository navigation
Expand file tree
/
Copy pathdocument.go
More file actions
458 lines (400 loc) · 15.3 KB
/
Copy pathdocument.go
File metadata and controls
458 lines (400 loc) · 15.3 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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
package cera
import (
"fmt"
"io"
"math"
"runtime"
"sync"
"github.com/timzifer/cera/internal/pdf"
"github.com/timzifer/cera/internal/cmyk"
)
// Document is an open PDF file. It is safe for concurrent use: pages of one
// Document may be rendered from many goroutines at once, and what they read
// from the file (objects, decoded streams, fonts, images, colour spaces) is
// loaded once and shared. Once a page has been rendered at a scale, further
// renders of it at that scale only draw its display list.
type Document struct {
*docShared
cmykView
}
// docShared is what a document has once for all its views (cmykView).
type docShared struct {
r *pdf.Document
fontProvider FontProvider // fonts the document does not embed
naiveCMYK bool // OpenOptions.NaiveCMYK
ignoreIntent bool // OpenOptions.IgnoreOutputIntent
intentOnce sync.Once
intent *colorSpace // DeviceCMYK through the output intent, nil without one
intentApprox string // what an output intent cera cannot use is counted as
fontMu sync.Mutex
fonts map[pdf.Ref]*Font // loaded fonts, by reference
// Decoded images: the document's own cache or the one it shares
// (OpenOptions.Cache), which knows its images by the id of a view.
imgs *imageCache
sharedCache bool // imgs is an OpenOptions.Cache
ocOnce sync.Once
oc *ocProps // optional content, read on first use
formOnce sync.Once
form *Form // the interactive form, read on first use
xfa bool // the form is an XFA form
ff formFonts // fonts of generated widget appearances
pageIdxOnce sync.Once
pageIdx map[pdf.Ref]int // page index by reference, for links
outlineOnce sync.Once
outline []OutlineItem // the document outline, read on first use
sigOnce sync.Once
sigs []Signature // the signatures, read on first use
attachOnce sync.Once
attachments []*Attachment // the embedded files, read on first use
labelOnce sync.Once
labels []labelRange // the page labels, read on first use
labelIdxOnce sync.Once
labelIdx map[string]int // the first page of each label
structOnce sync.Once
structMCIDs map[int][]int // marked-content identifiers by page, in structure order
structTreeOnce sync.Once
structTree *StructTree // the structure tree, read on first use
// CMYK profiles by profile object — a nil space for one that is not
// used, with the key it is counted under — and their tables, at most
// maxCMYKProfiles; under csMu.
csMu sync.Mutex
iccSpaces map[pdf.Ref]csEntry
iccTables int // grey and RGB profiles tabulated
cmykTables map[*cmyk.Table]bool
// Transfer functions by ExtGState reference.
trMu sync.Mutex
transfers map[pdf.Ref]trEntry
// The views of pages with an output intent of their own (Page.cmykDoc):
// by DeviceCMYK, and by page.
viewMu sync.Mutex
views map[viewKey]*Document
pageViews map[pdf.Ref]*Document
}
// cmykView is DeviceCMYK as a document converts it, and what is made of
// it: colour spaces, shadings and patterns by reference, decoded images.
// The pages of a document convert it alike but those with an output
// intent of their own (PDF 2.0, 14.11.5), which are drawn with a view of
// the document of their own (Page.cmykDoc).
type cmykView struct {
devCMYK *colorSpace // DeviceCMYK as the options or the page convert it; nil: as the document says, see deviceCMYK
devApprox string // what devCMYK is counted as
proj proj // what colours are projected to (cmykout.go)
id uint64 // of the view's images in imgs
// Decodes in progress by reference.
imgMu sync.Mutex
imgFlights map[pdf.Ref]*imageFlight
// Colour spaces and shadings by reference.
spMu sync.Mutex
spaces map[pdf.Ref]csEntry
shMu sync.Mutex
shadings map[pdf.Ref]*shadingEntry
patterns map[pdf.Ref]*patternEntry
}
// newDocument returns a document of r, converting DeviceCMYK as it says.
func newDocument(r *pdf.Document) *Document {
return &Document{docShared: &docShared{r: r}, cmykView: cmykView{id: docIDs.Add(1)}}
}
// Open parses a PDF file. Damaged cross-reference tables, wrong stream
// lengths and missing objects are repaired by the reader where possible.
// It returns [ErrPassword] for an encrypted file the empty password does
// not open, and [ErrUnsupportedEncryption] for one cera cannot decrypt.
func Open(data []byte) (*Document, error) {
return OpenWith(data, OpenOptions{})
}
// OpenWithPassword parses an encrypted PDF file. It is OpenWith with a
// password.
func OpenWithPassword(data []byte, password string) (*Document, error) {
return OpenWith(data, OpenOptions{Password: password})
}
// OpenWith parses a PDF file with options: a password, a provider of
// fonts the document does not embed, how CMYK is converted, and how much
// the document keeps decoded. It fails on a CMYKProfile it cannot read.
func OpenWith(data []byte, opt OpenOptions) (doc *Document, err error) {
return open(func(o pdf.Options) (*pdf.Document, error) { return pdf.OpenWith(data, o) }, opt)
}
// OpenReaderAt is OpenWith for a file of size bytes read through r as it
// is needed, rather than held in memory: a large file, or one in a browser
// read through the File API. r must not change while the Document is used,
// and must be safe for concurrent use if pages are rendered concurrently,
// as an *os.File is. A file with damaged cross-reference tables is read in
// full once to rebuild them.
func OpenReaderAt(r io.ReaderAt, size int64, opt OpenOptions) (doc *Document, err error) {
return open(func(o pdf.Options) (*pdf.Document, error) { return pdf.OpenReaderAt(r, size, o) }, opt)
}
func open(read func(pdf.Options) (*pdf.Document, error), opt OpenOptions) (doc *Document, err error) {
defer recoverPanic(&err)
var devCMYK *colorSpace
if opt.CMYKProfile != nil {
t, err := cmyk.Load(opt.CMYKProfile)
if err != nil {
return nil, fmt.Errorf("cera: CMYKProfile: %w", err)
}
devCMYK = &colorSpace{kind: csCMYK, n: 4, cmyk: t}
}
if opt.NaiveCMYK {
devCMYK = spaceCMYKNaive
}
po := pdf.Options{Password: opt.Password}
var imgs *imageCache
if c := opt.Cache; c != nil {
po.StreamCache, imgs = c.streams, c.images
} else {
// The reader takes 0 for its default and a negative number for none.
po.StreamCacheBytes = int(max(min(opt.StreamCacheBytes, math.MaxInt), -1))
imgs = newImageCache(opt.ImageCacheBytes)
}
d, err := read(po)
if err != nil {
return nil, openError(err)
}
doc = newDocument(d)
doc.fontProvider, doc.naiveCMYK, doc.ignoreIntent = opt.Fonts, opt.NaiveCMYK, opt.IgnoreOutputIntent
doc.imgs, doc.sharedCache = imgs, opt.Cache != nil
doc.devCMYK = devCMYK
doc.dropWithCache()
return doc, nil
}
// dropWithCache drops, when d is collected, what a shared cache has of
// it: a shared cache outlives the document.
func (d *Document) dropWithCache() {
if d.sharedCache {
imgs := d.imgs
runtime.AddCleanup(d, func(id uint64) { imgs.drop(id) }, d.id)
}
}
// NumPages returns the number of pages.
func (d *Document) NumPages() int { return d.r.PageCount() }
// Page returns page i (0-based). Pages are lightweight; nothing is
// interpreted until the page is rendered.
func (d *Document) Page(i int) (p *Page, err error) {
defer recoverPanic(&err)
if i < 0 || i >= d.NumPages() {
return nil, fmt.Errorf("cera: page %d out of range [0, %d)", i, d.NumPages())
}
pd, err := d.r.Page(i + 1)
if err != nil {
return nil, fmt.Errorf("cera: %w", err)
}
p = &Page{doc: d, index: i, dict: pd}
p.geometry()
return p, nil
}
// Rect is an axis-aligned rectangle.
type Rect struct{ X0, Y0, X1, Y1 float64 }
// Dx returns the width of r.
func (r Rect) Dx() float64 { return r.X1 - r.X0 }
// Dy returns the height of r.
func (r Rect) Dy() float64 { return r.Y1 - r.Y0 }
// letter is used when a page has no usable MediaBox.
var letter = Rect{0, 0, 612, 792}
// Page is one page of a Document. It caches the display list of its last
// render; Release frees it.
//
// A viewer that turns its view uses [Page.Rotated]: the page as it is
// shown is then a Page of its own, and everything given in points of the
// displayed page — Bounds, Render, the text, links, annotations — is in
// its coordinates.
type Page struct {
doc *Document
index int
dict pdf.Dict
// Box is the visible area in default user space: CropBox clipped to
// MediaBox, or for a page made by InBox the box it shows. Its units
// are /UserUnit points; Size and DisplayMatrix apply it.
Box Rect
// Rotate is the clockwise display rotation: 0, 90, 180 or 270, the
// /Rotate of the page and what Rotated added to it. It is for
// reading: a page is rendered, measured and cached as it was made, so
// setting Rotate after its first use draws one rotation in the size
// of another. Use Rotated for another rotation.
Rotate int
// unit is /UserUnit (points per user-space unit), 1 by default.
unit float64
// boxes are the boxes of the page; see Boxes.
boxes PageBoxes
mu sync.Mutex
dl *displayList // cached by Render; see Release
// flights are the lists being recorded; see list.
flights map[listKey]*listFlight
// wl are the lists of widgets showing values of a FormState, by
// annotation index; see Render.
wl map[int]*widgetList
annOnce sync.Once
annots []Annotation
annBad int // unreadable entries of /Annots
// root is the page Rotated or InBox was called on, nil for that page
// itself, turn what was added to its rotation and box the box shown.
// The root keeps its views, so that a view asked for again has its
// display list still.
root *Page
turn int
box PageBox
views [numBoxes][4]*Page // by box and quarter turn, but for the page itself; under mu
}
// PageBox names one of the boxes of a page.
type PageBox int
const (
BoxCrop PageBox = iota // what a viewer shows and a printer prints; the default
BoxMedia // the medium the page is printed on, with all its marks
BoxBleed // what is printed and then cut off around the trimmed page
BoxTrim // the finished page, after trimming
BoxArt // the content its author means, as for placing the page in another
numBoxes
)
// PageBoxes are the boxes of a page in default user space, as Box is,
// each clipped to the MediaBox, with the defaults of the specification
// for one the page does not have or has no area of within the MediaBox:
// Crop is then Media, and Bleed, Trim and Art are Crop.
type PageBoxes struct{ Media, Crop, Bleed, Trim, Art Rect }
// Box returns the box b names, and Crop for none of them.
func (b PageBoxes) Box(box PageBox) Rect {
switch box {
case BoxMedia:
return b.Media
case BoxBleed:
return b.Bleed
case BoxTrim:
return b.Trim
case BoxArt:
return b.Art
}
return b.Crop
}
// Boxes returns the boxes of the page. They are in default user space
// whatever the page shows; [Page.DisplayMatrix] maps them to points of the
// displayed page, where a viewer of print files outlines them.
func (p *Page) Boxes() PageBoxes { return p.boxes }
// InBox returns the page showing another of its boxes than the CropBox:
// the MediaBox with the marks around a page for print, or the TrimBox for
// a thumbnail of the finished page. It is a view of the page as
// [Page.Rotated] makes one, with Box, Size, Bounds and DisplayMatrix of
// that box; the two combine, in either order. Render draws what is inside
// the box, and "points of the displayed page" have their origin at its
// corner. InBox(BoxCrop) is the page as the document shows it.
//
// APIs that take a page number ask for the box as they do for the
// rotation: [TextIndex.SetBox], [NewAnnotation.Box], [Highlight.Box].
func (p *Page) InBox(box PageBox) *Page {
root, turn, _ := p.origin()
if box < 0 || box >= numBoxes {
box = BoxCrop
}
return root.view(box, turn)
}
// Rotated returns the page viewed turned by degrees more, clockwise: a
// multiple of 90, others are rounded to one. It is the page itself for a
// multiple of 360, and else a view of it — a Page with Rotate turned, its
// own bounds, display list and text, and the same content. Asked for
// again, the same view is returned, so what it cached is still there;
// Release on the page frees the views' lists too.
//
// "Points of the displayed page" are those of the view everywhere: of
// what Render draws, of [PageText], of links, [Annotation.Markup] and
// [Page.EditAnnotation], whose result [Editor.SetAnnotation] takes back
// with its view. APIs that take a page number ask for the view where it
// matters: [Page.Destination], [TextIndex.SetRotation],
// [NewAnnotation.View], [Highlight.View].
func (p *Page) Rotated(degrees int) *Page {
root, turn, box := p.origin()
q := int(math.Round(float64(degrees) / 90))
return root.view(box, ((turn+q*90)%360+360)%360)
}
// origin returns the page p is a view of, p itself if it is none, and how
// p views it.
func (p *Page) origin() (root *Page, turn int, box PageBox) {
if p.root != nil {
return p.root, p.turn, p.box
}
return p, 0, BoxCrop
}
// view returns the view of p, a page that is no view, that shows box
// turned by turn, one of 0, 90, 180 and 270.
func (p *Page) view(box PageBox, turn int) *Page {
if box == BoxCrop && turn == 0 {
return p
}
p.mu.Lock()
defer p.mu.Unlock()
v := &p.views[box][turn/90]
if *v == nil {
shown := p.Box
if box != BoxCrop {
shown = p.boxes.Box(box)
}
*v = &Page{doc: p.doc, index: p.index, dict: p.dict, Box: shown, boxes: p.boxes, unit: p.unit,
Rotate: (p.Rotate + turn) % 360, root: p, turn: turn, box: box}
}
return *v
}
// Index returns the 0-based page number.
func (p *Page) Index() int { return p.index }
// Size returns the displayed size in points, after /Rotate and /UserUnit.
func (p *Page) Size() (w, h float64) {
w, h = p.Box.Dx()*p.unit, p.Box.Dy()*p.unit
if p.Rotate == 90 || p.Rotate == 270 {
return h, w
}
return w, h
}
func (p *Page) geometry() {
d := p.doc
media, ok := d.rect(p.dict.Get("MediaBox"))
if !ok {
media = letter
}
// box returns the box of the page under key within the MediaBox, and
// def for none with an area there.
box := func(key pdf.Name, def Rect) Rect {
if r, ok := d.rect(p.dict.Get(key)); ok {
c := Rect{max(r.X0, media.X0), max(r.Y0, media.Y0), min(r.X1, media.X1), min(r.Y1, media.Y1)}
if c.Dx() > 0 && c.Dy() > 0 {
return c
}
}
return def
}
crop := box("CropBox", media)
p.boxes = PageBoxes{Media: media, Crop: crop, Bleed: box("BleedBox", crop), Trim: box("TrimBox", crop), Art: box("ArtBox", crop)}
p.Box = crop
if v, ok := d.num(p.dict.Get("Rotate")); ok {
r := int(math.Mod(math.Round(v/90), 4))
p.Rotate = (r + 4) % 4 * 90
}
p.unit = 1
if v, ok := d.num(p.dict.Get("UserUnit")); ok && v > 0 && v < 1000 {
p.unit = v
}
}
// resolve follows references; broken ones become null.
func (d *Document) resolve(o pdf.Object) pdf.Object { return d.r.Resolve(o) }
func (d *Document) dict(o pdf.Object) pdf.Dict {
v, _ := d.resolve(o).Dict()
return v
}
func (d *Document) num(o pdf.Object) (float64, bool) {
f, ok := d.resolve(o).Float()
if !ok || math.IsNaN(f) || math.IsInf(f, 0) {
return 0, false
}
return f, true
}
func (d *Document) name(o pdf.Object) (pdf.Name, bool) {
return d.resolve(o).Name()
}
func (d *Document) rect(o pdf.Object) (Rect, bool) {
a, ok := d.resolve(o).Array()
if !ok || len(a) != 4 {
return Rect{}, false
}
var v [4]float64
for i, e := range a {
if v[i], ok = d.num(e); !ok {
return Rect{}, false
}
}
r := Rect{min(v[0], v[2]), min(v[1], v[3]), max(v[0], v[2]), max(v[1], v[3])}
if r.Dx() <= 0 || r.Dy() <= 0 {
return Rect{}, false
}
return r, true
}