-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrender.go
More file actions
270 lines (254 loc) · 9.03 KB
/
Copy pathrender.go
File metadata and controls
270 lines (254 loc) · 9.03 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
// Package render turns a page of a PDF into pixels.
//
// It reads with github.com/go-pdfkit/reader and rasterises with
// github.com/go-gfx/gfx. Nothing outside those and the standard library is
// used, so it builds for js/wasm and for every architecture the fleet targets
// — which is what lets a PDF be shown in a browser tab with nothing on the far
// end.
package render
import (
"errors"
"fmt"
"math"
"time"
"github.com/go-gfx/gfx/geometry"
"github.com/go-gfx/gfx/raster"
"github.com/go-pdfkit/reader"
"image/color"
)
// Options say how a page is to be drawn.
type Options struct {
// Scale is device pixels per point. Zero means one, which is seventy-two
// to the inch; [Options.DPI] is the same thing said the other way.
Scale float64
// DPI sets the scale from dots per inch. It is ignored when Scale is set.
DPI float64
// Background is painted before anything else. The zero value is opaque
// white, which is what paper is.
Background *color.RGBA
// MaxPixels refuses a page that would come out larger than this, so a
// media box a file made up cannot be asked to fill memory. Zero means
// forty million, which is a little over A4 at 600 dots to the inch.
MaxPixels int
// NoAnnotations leaves out what the page carries beside its content: the
// notes, the highlights, the stamps, and the fields of a form. They are
// drawn by default, because a filled-in form drawn without them is a form
// with nothing in it.
NoAnnotations bool
// MaxDuration is how long the page may be drawn for. Zero means as long as
// it takes, which is what this did before there was anywhere to say
// otherwise.
//
// Some pages take a very long time. Of 59 432 corpus pages drawn at half
// as much again as their own size, 1 131 were still going after twenty
// seconds, and one figure took two hundred and seventy-three. A caller
// drawing somebody else's file — a browser tab, a queue of them, a server
// — cannot afford to wait for the worst of those and has, until now, had
// no way to say so: MaxPixels bounds what comes out, and nothing bounded
// the work of making it.
//
// When the time runs out the page stops being drawn and comes back as far
// as it got, with ErrTimedOut. That is deliberately not a blank: half a
// page is worth more than nothing to somebody scrolling, and the error
// says plainly that it is half.
MaxDuration time.Duration
// AllLayers draws every optional-content layer, including the ones the
// document's default configuration turns off.
//
// The default is to draw what the document says it shows when it is
// opened. On the corpus that is the same picture either way: 275 of the
// 1 633 real forms use optional content and 161 hide a group, but not one
// mark on any page of any of the 8 300 files measured falls inside a layer
// its own configuration hides. See optional.go for the numbers and for why
// the mechanism is read all the same.
//
// A caller that wants everything — a tool showing a layer panel, an
// archive comparing what a file holds against what it displays — sets
// this and gets the behaviour from before optional content was read.
AllLayers bool
}
// ErrTimedOut says a page was still being drawn when its time ran out. The
// image returned with it holds what had been drawn by then.
var ErrTimedOut = errors.New("render: the page was still being drawn when its time ran out")
// defaultMaxPixels is a little more than A4 at 600 dots to the inch.
const defaultMaxPixels = 40 << 20
// scale works out how many device pixels there are to a point.
func (o Options) scale() float64 {
switch {
case o.Scale > 0:
return o.Scale
case o.DPI > 0:
return o.DPI / 72
}
return 1
}
// background is the colour the paper starts as.
func (o Options) background() color.RGBA {
if o.Background != nil {
return *o.Background
}
return color.RGBA{R: 255, G: 255, B: 255, A: 255}
}
// maxPixels is how large a page may be.
func (o Options) maxPixels() int {
if o.MaxPixels > 0 {
return o.MaxPixels
}
return defaultMaxPixels
}
// Page draws the i'th page of a document, counting from one.
func Page(d *reader.Document, i int, opt Options) (*raster.Image, error) {
page, err := d.Page(i)
if err != nil {
return nil, err
}
box := pageBox(d, page)
rotation := pageRotation(d, page)
s := opt.scale()
// A box with no extent is not a box, and a scale is never zero, so
// rounding up always leaves at least one pixel.
w := int(math.Ceil((box[2] - box[0]) * s))
h := int(math.Ceil((box[3] - box[1]) * s))
if rotation == 90 || rotation == 270 {
w, h = h, w
}
if w*h > opt.maxPixels() {
return nil, fmt.Errorf("render: page %d would be %d by %d pixels, past the limit of %d", i, w, h, opt.maxPixels())
}
img := raster.New(w, h)
fill(img, opt.background())
// A page's content is drawn as far as it decoded. 263 streams in 212 of
// the 1 633 real forms cannot be decoded cleanly, and a strict read hands
// back nothing for all of them; this package already draws a page as far as
// it got when its time runs out, and a damaged stream is the same
// situation arriving another way.
//
// Nothing that no filter decoded is ever run: the reader keeps those bytes
// in Undecoded, set instead of Data rather than beside it, so taking Data
// cannot reach them. A page whose /Contents is filtered as an image — a
// JPEG where operators should be — arrives here with Data empty and a
// cause, and is refused rather than run.
// The error is dropped: PageContentDecoded returns one only when the page
// itself cannot be read, and it was read a few lines above. A /Contents
// that points at an object the file does not hold resolves to nothing
// rather than to an error, and a page with no content is a blank page —
// which is what this drew before and what other readers draw.
dec, _ := d.PageContentDecoded(i)
if len(dec.Data) == 0 && dec.Cause != nil {
return nil, dec.Cause
}
content := dec.Data
resources, _ := d.GetDict(page, "Resources")
r := &renderer{doc: d, img: img, fonts: map[int]*pdfFont{}, softMasks: map[softMaskKey][]uint8{}}
if !opt.AllLayers {
r.oc = readOptional(d)
}
if opt.MaxDuration > 0 {
r.deadline = time.Now().Add(opt.MaxDuration)
}
start := r.initialState(box, rotation, s)
r.base = start.ctm
r.run(content, resources, start)
if !opt.NoAnnotations {
r.drawAnnotations(page, start)
}
if r.ranOut {
return img, ErrTimedOut
}
return img, nil
}
// fill paints the whole image one colour.
func fill(img *raster.Image, c color.RGBA) {
for y := 0; y < img.H; y++ {
for x := 0; x < img.W; x++ {
img.Set(x, y, c)
}
}
}
// initialState is the graphics state a page starts in: the transform that maps
// its own coordinates onto the image, and the defaults every page begins with.
func (r *renderer) initialState(box [4]float64, rotation int, s float64) gstate {
// PDF counts upwards from the bottom left of the box; an image counts
// downwards from its top left.
flip := geometry.Matrix{
Xx: s, Yx: 0, X0: -box[0] * s,
Xy: 0, Yy: -s, Y0: box[3] * s,
}
w, h := (box[2]-box[0])*s, (box[3]-box[1])*s
var turn geometry.Matrix
switch rotation {
case 90:
turn = geometry.Matrix{Xx: 0, Yx: -1, X0: h, Xy: 1, Yy: 0, Y0: 0}
case 180:
turn = geometry.Matrix{Xx: -1, Yx: 0, X0: w, Xy: 0, Yy: -1, Y0: h}
case 270:
turn = geometry.Matrix{Xx: 0, Yx: 1, X0: 0, Xy: -1, Yy: 0, Y0: w}
default:
turn = geometry.Identity()
}
return gstate{
ctm: turn.Mul(flip),
fill: color.RGBA{A: 255},
stroke: color.RGBA{A: 255},
fillSpace: deviceGray,
strokeSpace: deviceGray,
lineWidth: 1,
miterLimit: 10,
fillAlpha: 1,
strokeAlpha: 1,
}
}
// pageBox is the area of the page that gets drawn: what it says should be
// visible, or the paper it is on.
func pageBox(d *reader.Document, page reader.Dict) [4]float64 {
for _, key := range []reader.Name{"CropBox", "MediaBox"} {
if b, ok := rectangle(d, page.Get(key)); ok {
return b
}
}
return [4]float64{0, 0, 612, 792}
}
// pageRotation reads a page's /Rotate, normalised to a right angle.
func pageRotation(d *reader.Document, page reader.Dict) int {
n, ok := reader.ToInt(resolve(d, page.Get("Rotate")))
if !ok || n%90 != 0 {
return 0
}
deg := int(n) % 360
if deg < 0 {
deg += 360
}
return deg
}
// rectangle reads a PDF rectangle, normalised so the first corner is the lower
// left one — files do write them the other way round.
func rectangle(d *reader.Document, o reader.Object) ([4]float64, bool) {
var out [4]float64
arr, ok := reader.ToArray(resolve(d, o))
if !ok || len(arr) < 4 {
return out, false
}
for i := 0; i < 4; i++ {
v, ok := reader.ToFloat(resolve(d, arr[i]))
if !ok {
return out, false
}
out[i] = v
}
if out[0] > out[2] {
out[0], out[2] = out[2], out[0]
}
if out[1] > out[3] {
out[1], out[3] = out[3], out[1]
}
if out[0] == out[2] || out[1] == out[3] {
return out, false
}
return out, true
}
// resolve follows an indirect reference. A document that opened cannot fail to
// resolve one, so there is nothing to handle at every use.
func resolve(d *reader.Document, o reader.Object) reader.Object {
out, _ := d.Resolve(o)
return out
}