601 lines
24 KiB
Odin
601 lines
24 KiB
Odin
package pdf
|
|||
|
|
|
||
|
|
import "core:math"
|
||
|
|
import "core:mem"
|
||
|
|
import "core:mem/virtual"
|
||
|
|
|
||
|
|
// The public rendering API: what a user of this library calls once a Document
|
||
|
|
// is open.
|
||
|
|
//
|
||
|
|
// doc, _ := pdf.open_file("thesis.pdf")
|
||
|
|
// page, _ := pdf.page_get(doc, 0)
|
||
|
|
// bmp, _ := pdf.render_page(&page, {ctm = pdf.scale_matrix(2, 2)})
|
||
|
|
// defer pdf.bitmap_destroy(&bmp)
|
||
|
|
//
|
||
|
|
// Everything below this file is already written: `content.odin` walks the
|
||
|
|
// operators, the three devices turn them into pixels, text or a display list.
|
||
|
|
// What is here is the small amount that only a *page* knows -- where the page
|
||
|
|
// sits in device space, how big a bitmap it needs, which scratch memory the
|
||
|
|
// run may use, and that a page is its content stream *plus* its annotations.
|
||
|
|
//
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// THE BASE MATRIX
|
||
|
|
//
|
||
|
|
// This is the piece everything else depends on and the easiest to get subtly
|
||
|
|
// wrong, so it is derived once, in `page_base_matrix`, and every entry point
|
||
|
|
// here goes through it. That is what makes a rendered bitmap, extracted text
|
||
|
|
// and a display list agree about where things are instead of being three
|
||
|
|
// nearly-identical derivations that disagree in one corner.
|
||
|
|
//
|
||
|
|
// PDF user space is y-UP with its origin at the bottom-left of the CROP box.
|
||
|
|
// A bitmap is y-DOWN with its origin at the top-left. So the base transform,
|
||
|
|
// in order:
|
||
|
|
//
|
||
|
|
// 1. translates the crop box's lower-left corner to the origin;
|
||
|
|
// 2. scales by /UserUnit, the size in points of one unit of page space;
|
||
|
|
// 3. flips y and applies /Rotate, mapping the page onto a device rectangle
|
||
|
|
// whose top-left corner is (0,0);
|
||
|
|
// 4. applies the caller's `ctm`.
|
||
|
|
//
|
||
|
|
// Steps 1-3 are fixed by the page. Step 4 is the caller's zoom, and is what
|
||
|
|
// `Render_Options.ctm`, `run_page`'s `ctm` and `page_display_list`'s `ctm` all
|
||
|
|
// are: a transform applied ON TOP of the flip and the rotation, never instead
|
||
|
|
// of them. `MATRIX_IDENTITY` therefore renders the page upright at one device
|
||
|
|
// pixel per point, which is what a caller expects to get by passing nothing.
|
||
|
|
//
|
||
|
|
// The four rotations are written out as constants rather than built with
|
||
|
|
// `rotate_matrix`, because cos(pi/2) is 6.1e-17 and not 0: a page with
|
||
|
|
// /Rotate 90 must land on exact pixel boundaries, not 6.1e-17 off them.
|
||
|
|
//
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// MEMORY
|
||
|
|
//
|
||
|
|
// Three kinds, and they must not be confused:
|
||
|
|
//
|
||
|
|
// * The Document's arena holds everything parsed out of the file -- fonts,
|
||
|
|
// images, colour spaces, the decoded content bytes. It is never freed
|
||
|
|
// before `close`, so nothing per-render may go in it.
|
||
|
|
// * Per-page scratch -- the operand stack, paths under construction, glyph
|
||
|
|
// runs, tile lattices, the strings the content lexer materialises -- goes
|
||
|
|
// in a `virtual.Arena` that is a LOCAL of the entry point that started the
|
||
|
|
// render. It is created when the call starts and destroyed when it
|
||
|
|
// returns, so a 900-page loop peaks at one page's scratch rather than
|
||
|
|
// growing without bound. That is the arena ARCHITECTURE.md asks for, and
|
||
|
|
// `page_scratch_begin`/`page_scratch_end` are where it lives.
|
||
|
|
// * The result -- `render_page`'s Bitmap, `extract_text`'s string,
|
||
|
|
// `page_display_list`'s list -- belongs to the CALLER, comes from the
|
||
|
|
// allocator the caller named, and is freed by the caller.
|
||
|
|
//
|
||
|
|
// The devices themselves allocate from `context.allocator` and free everything
|
||
|
|
// in their own `*_destroy`; they are built and torn down inside one call.
|
||
|
|
//
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// WHAT A PAGE IS
|
||
|
|
//
|
||
|
|
// A page is its content stream AND its annotations. `render_page`,
|
||
|
|
// `extract_text` and `page_display_list` all draw both, because a form field's
|
||
|
|
// value and a stamp are as much a part of what the page looks like as the text
|
||
|
|
// under them -- /Annots is in half the corpus. `annot.odin` owns the placement
|
||
|
|
// and this file owns the order: content first, annotations over it, in
|
||
|
|
// /Annots order. `run_page` is the one exception and says so.
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Options
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// Render_Options tunes one call to `render_page` or `render_page_to`. The zero
|
||
|
|
// value is the sensible default for all of them: an opaque white page, no
|
||
|
|
// extra transform, annotations drawn.
|
||
|
|
Render_Options :: struct {
|
||
|
|
// Applied on top of the page flip and /Rotate, never instead of them. The
|
||
|
|
// zero Matrix is not a transform, so it is read as MATRIX_IDENTITY.
|
||
|
|
ctm: Matrix,
|
||
|
|
|
||
|
|
// RGBA, straight alpha. An all-zero value means "unset" -- which is what a
|
||
|
|
// zeroed Render_Options carries -- and is filled in as opaque white for an
|
||
|
|
// opaque page and left fully transparent when `alpha` is set. Any other
|
||
|
|
// value is used as given, with its alpha forced to 255 unless `alpha` is
|
||
|
|
// set, because an opaque page cannot start out see-through.
|
||
|
|
background: [4]u8,
|
||
|
|
|
||
|
|
// Start the page transparent and keep whatever alpha the content leaves.
|
||
|
|
// The bitmap is RGBA8 either way; this decides what is underneath.
|
||
|
|
alpha: bool,
|
||
|
|
|
||
|
|
// Draw the content stream only. The default draws the page's annotation
|
||
|
|
// appearance streams over it, which is what a viewer shows.
|
||
|
|
no_annotations: bool,
|
||
|
|
|
||
|
|
// Where the returned Bitmap's pixels come from. Zero means
|
||
|
|
// context.allocator. It is not used for any scratch memory.
|
||
|
|
allocator: mem.Allocator,
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// The base matrix
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// page_base_matrix maps the page's user space to device space, with `ctm`
|
||
|
|
// applied on top.
|
||
|
|
//
|
||
|
|
// It is the transform every entry point in this file renders through, and the
|
||
|
|
// one a caller needs to relate what came out of `render_page` to what is in
|
||
|
|
// the file: `transform_point(page_base_matrix(&page, ctm), p)` is where user
|
||
|
|
// point `p` lands in the bitmap, and the inverse (via `matrix_invert`) turns a
|
||
|
|
// pixel a user clicked on back into a point on the page.
|
||
|
|
//
|
||
|
|
// The origin is the CROP box's lower-left corner, not the media box's: the
|
||
|
|
// crop box is the rectangle a viewer displays, and `render_page` sizes its
|
||
|
|
// bitmap to it. /UserUnit and /Rotate are both folded in, so a page with
|
||
|
|
// /Rotate 90 comes back landscape and the right way up.
|
||
|
|
page_base_matrix :: proc(page: ^Page, ctm := MATRIX_IDENTITY) -> Matrix {
|
||
|
|
m := ctm
|
||
|
|
if !matrix_is_finite(m) {
|
||
|
|
// A caller-supplied matrix is not file data, but it can be arithmetic
|
||
|
|
// on file data. A NaN here would silently blank the page.
|
||
|
|
m = MATRIX_IDENTITY
|
||
|
|
}
|
||
|
|
if page == nil {
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
crop := page_bounds(page)
|
||
|
|
unit := page.user_unit
|
||
|
|
if !is_finite(unit) || unit <= 0 {
|
||
|
|
unit = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// The page's extent in device units before rotation.
|
||
|
|
w := rect_width(crop) * unit
|
||
|
|
h := rect_height(crop) * unit
|
||
|
|
if !is_finite(w) || !is_finite(h) {
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
// Crop origin to (0,0), then /UserUnit.
|
||
|
|
base := Matrix{unit, 0, 0, unit, -crop.x0 * unit, -crop.y0 * unit}
|
||
|
|
|
||
|
|
// The flip and the rotation in one step. Each maps the [0,w] x [0,h] y-up
|
||
|
|
// rectangle onto the y-down device rectangle a viewer would show, whose
|
||
|
|
// top-left corner is the origin. Every one has determinant -1: the y-flip
|
||
|
|
// reverses orientation once, and a rotation does not reverse it again.
|
||
|
|
flip: Matrix
|
||
|
|
switch page.rotate {
|
||
|
|
case 90:
|
||
|
|
// Clockwise: the page's left edge becomes the top of the image, so the
|
||
|
|
// device is h wide and w tall.
|
||
|
|
flip = Matrix{0, 1, 1, 0, 0, 0}
|
||
|
|
case 180:
|
||
|
|
flip = Matrix{-1, 0, 0, 1, w, 0}
|
||
|
|
case 270:
|
||
|
|
flip = Matrix{0, -1, -1, 0, h, w}
|
||
|
|
case:
|
||
|
|
flip = Matrix{1, 0, 0, -1, 0, h}
|
||
|
|
}
|
||
|
|
|
||
|
|
return matrix_mul(matrix_mul(base, flip), m)
|
||
|
|
}
|
||
|
|
|
||
|
|
// page_render_size is the pixel size `render_page` allocates for `ctm`.
|
||
|
|
//
|
||
|
|
// The bitmap spans device space from (0,0) to the far corner of the page under
|
||
|
|
// the full transform, so for the transforms callers actually use -- a scale,
|
||
|
|
// or nothing -- it is exactly `page_size`, /Rotate and /UserUnit included. A
|
||
|
|
// `ctm` that pushes content to negative coordinates pushes it off the bitmap
|
||
|
|
// rather than moving the bitmap: that is what makes `render_page_to` usable
|
||
|
|
// for tiled rendering, where each tile's `ctm` carries the translation that
|
||
|
|
// selects it.
|
||
|
|
@(private)
|
||
|
|
page_render_size :: proc(page: ^Page, ctm: Matrix) -> (width, height: int, err: Error) {
|
||
|
|
if page == nil {
|
||
|
|
return 0, 0, .Invalid_Argument
|
||
|
|
}
|
||
|
|
box := transform_rect(page_base_matrix(page, ctm), page_bounds(page))
|
||
|
|
if !rect_is_finite(box) {
|
||
|
|
return 0, 0, .Invalid_Argument
|
||
|
|
}
|
||
|
|
|
||
|
|
// Rounded outward with the same epsilon page_size uses: a page 100.2
|
||
|
|
// pixels wide needs 101 columns, but an exact 200 that floating point
|
||
|
|
// rendered as 200.00000000000003 must not claim a 201st.
|
||
|
|
limit := f64(MAX_BITMAP_PIXELS)
|
||
|
|
if box.x1 > limit || box.y1 > limit {
|
||
|
|
return 0, 0, .Bitmap_Too_Large
|
||
|
|
}
|
||
|
|
width = max(1, int(math.ceil(box.x1 - 1e-9)))
|
||
|
|
height = max(1, int(math.ceil(box.y1 - 1e-9)))
|
||
|
|
if width > MAX_BITMAP_PIXELS / height {
|
||
|
|
return 0, 0, .Bitmap_Too_Large
|
||
|
|
}
|
||
|
|
return width, height, .None
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Scratch memory
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// Page_Scratch is the per-page arena. It is held by value in the caller's
|
||
|
|
// frame so that it dies with the call: see the memory note at the top.
|
||
|
|
@(private)
|
||
|
|
Page_Scratch :: struct {
|
||
|
|
arena: virtual.Arena,
|
||
|
|
alloc: mem.Allocator,
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
page_scratch_begin :: proc(s: ^Page_Scratch) -> Error {
|
||
|
|
if virtual.arena_init_growing(&s.arena) != nil {
|
||
|
|
return .Out_Of_Memory
|
||
|
|
}
|
||
|
|
s.alloc = virtual.arena_allocator(&s.arena)
|
||
|
|
return .None
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
page_scratch_end :: proc(s: ^Page_Scratch) {
|
||
|
|
virtual.arena_destroy(&s.arena)
|
||
|
|
s^ = {}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Running a page into a device
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// run_page runs the page's content stream into `dev`.
|
||
|
|
//
|
||
|
|
// `ctm` is applied on top of the page flip and /Rotate, exactly like
|
||
|
|
// `Render_Options.ctm`: pass MATRIX_IDENTITY to draw the page upright at one
|
||
|
|
// device pixel per point. This is the entry point for a caller with a device
|
||
|
|
// of their own -- a GPU backend, a bounding-box collector, an SVG writer.
|
||
|
|
//
|
||
|
|
// Annotations are NOT drawn: this runs the page, and an annotation is a
|
||
|
|
// separate object with its own appearance stream. There is no option here to
|
||
|
|
// say otherwise, which is deliberate -- a caller that wants a page's
|
||
|
|
// annotations as device calls records the whole page with `page_display_list`
|
||
|
|
// and replays it, and a caller that wants only the content stream has this.
|
||
|
|
//
|
||
|
|
// The device's `close` hook is called when the page is finished, which is what
|
||
|
|
// balances any group or clip the content left open. Nothing about a malformed
|
||
|
|
// content stream is fatal: everything that could not be run is recorded as a
|
||
|
|
// Repair on the Document and the rest still draws.
|
||
|
|
run_page :: proc(page: ^Page, dev: ^Device, ctm: Matrix) -> Error {
|
||
|
|
if page == nil || page.doc == nil {
|
||
|
|
return .Invalid_Argument
|
||
|
|
}
|
||
|
|
m := page_ctm_or_identity(ctm)
|
||
|
|
if !matrix_is_finite(m) {
|
||
|
|
return .Invalid_Argument
|
||
|
|
}
|
||
|
|
|
||
|
|
scratch: Page_Scratch
|
||
|
|
page_scratch_begin(&scratch) or_return
|
||
|
|
defer page_scratch_end(&scratch)
|
||
|
|
|
||
|
|
return page_run(page, dev, m, &scratch, false, false)
|
||
|
|
}
|
||
|
|
|
||
|
|
// page_run is the one implementation the four public entry points share: run
|
||
|
|
// the content, then the annotations, then close the device.
|
||
|
|
//
|
||
|
|
// `text` selects the settings text extraction wants and rendering does not --
|
||
|
|
// chiefly drawing optional content the catalog's default configuration hides,
|
||
|
|
// because hidden text is still text.
|
||
|
|
@(private)
|
||
|
|
page_run :: proc(page: ^Page, dev: ^Device, ctm: Matrix, scratch: ^Page_Scratch,
|
||
|
|
annots: bool, text: bool) -> Error {
|
||
|
|
doc := page.doc
|
||
|
|
base := page_base_matrix(page, ctm)
|
||
|
|
|
||
|
|
// The soft-mask and inline-image budgets are the page's, not each run's:
|
||
|
|
// the annotations below are separate `run_content` calls and would
|
||
|
|
// otherwise each get a fresh one. This lives in the page's scratch arena,
|
||
|
|
// which is exactly the lifetime `Content_Options.shared` asks for.
|
||
|
|
shared := new(Content_Shared, scratch.alloc)
|
||
|
|
opts := Content_Options{
|
||
|
|
allocator = scratch.alloc,
|
||
|
|
ignore_optional_content = text,
|
||
|
|
shared = shared,
|
||
|
|
}
|
||
|
|
|
||
|
|
// The content bytes are the concatenation of /Contents, which is scratch:
|
||
|
|
// a page whose content is 40 MB of Flate must not leave 40 MB in the
|
||
|
|
// Document's arena for the rest of the document's life.
|
||
|
|
content, cerr := page_content(page, scratch.alloc)
|
||
|
|
err := cerr
|
||
|
|
if cerr == .None && len(content) > 0 {
|
||
|
|
rerr := run_content(doc, dev, content, page.resources, base, opts)
|
||
|
|
if rerr != .None {
|
||
|
|
err = rerr
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Annotations are `annot.odin`'s: an appearance stream is a form XObject
|
||
|
|
// placed on the annotation's /Rect, and it runs through the same
|
||
|
|
// interpreter under the same CTM the content stream had. Sharing `opts`
|
||
|
|
// shares this page's scratch arena and its token budget with them.
|
||
|
|
if annots {
|
||
|
|
annot_run_page(doc, page, dev, base, opts)
|
||
|
|
}
|
||
|
|
|
||
|
|
dev_close(dev)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Rendering to a bitmap
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// render_page rasterises a page and returns the bitmap.
|
||
|
|
//
|
||
|
|
// The result is RGBA8 with STRAIGHT (un-premultiplied) alpha, sized to the
|
||
|
|
// page's crop box under `opts.ctm` with /Rotate and /UserUnit already applied,
|
||
|
|
// so a page with /Rotate 90 comes back landscape and the right way up. A
|
||
|
|
// request larger than MAX_BITMAP_PIXELS is refused with .Bitmap_Too_Large
|
||
|
|
// rather than attempted.
|
||
|
|
//
|
||
|
|
// THE BITMAP BELONGS TO THE CALLER. It is allocated from `opts.allocator`
|
||
|
|
// (context.allocator when that is zero) and must be released with
|
||
|
|
// `bitmap_destroy`. Nothing else the call touches survives it: the scratch
|
||
|
|
// arena and the raster device are both gone by the time it returns.
|
||
|
|
//
|
||
|
|
// A page that will not draw still returns a bitmap. Malformed content records
|
||
|
|
// Repairs on the Document and draws what it can, so a page that is 90%
|
||
|
|
// drawable comes back 90% drawn; an error return means the bitmap could not be
|
||
|
|
// made at all.
|
||
|
|
render_page :: proc(page: ^Page, opts := Render_Options{}) -> (Bitmap, Error) {
|
||
|
|
if page == nil || page.doc == nil {
|
||
|
|
return {}, .Invalid_Argument
|
||
|
|
}
|
||
|
|
ctm := page_ctm_or_identity(opts.ctm)
|
||
|
|
if !matrix_is_finite(ctm) {
|
||
|
|
return {}, .Invalid_Argument
|
||
|
|
}
|
||
|
|
|
||
|
|
width, height, serr := page_render_size(page, ctm)
|
||
|
|
if serr != .None {
|
||
|
|
return {}, serr
|
||
|
|
}
|
||
|
|
|
||
|
|
alloc := opts.allocator
|
||
|
|
if alloc.procedure == nil {
|
||
|
|
alloc = context.allocator
|
||
|
|
}
|
||
|
|
|
||
|
|
bmp, berr := bitmap_make(width, height, 4, alloc)
|
||
|
|
if berr != .None {
|
||
|
|
return {}, berr
|
||
|
|
}
|
||
|
|
|
||
|
|
resolved := opts
|
||
|
|
resolved.ctm = ctm
|
||
|
|
if rerr := render_page_to(page, &bmp, resolved); rerr != .None {
|
||
|
|
// The bitmap is still the caller's -- it is background-filled and
|
||
|
|
// holds whatever was drawn before the failure. Handing it back with
|
||
|
|
// the error is what lets a caller show a blank page instead of
|
||
|
|
// nothing.
|
||
|
|
return bmp, rerr
|
||
|
|
}
|
||
|
|
return bmp, .None
|
||
|
|
}
|
||
|
|
|
||
|
|
// render_page_to rasterises a page into a bitmap the caller already has.
|
||
|
|
//
|
||
|
|
// It paints the background and produces exactly the picture `render_page`
|
||
|
|
// would, so the two can be checked against each other; the only difference is
|
||
|
|
// who allocated. Any component count is accepted (1 grey, 2 grey+alpha, 3 RGB,
|
||
|
|
// 4 RGBA) and the bitmap is left with straight alpha.
|
||
|
|
//
|
||
|
|
// `page_size(page, scale)` is the size `render_page` allocates for
|
||
|
|
// `scale_matrix(scale, scale)`, so a caller that wants the whole page allocates
|
||
|
|
// that and gets the same picture.
|
||
|
|
//
|
||
|
|
// The bitmap is NOT resized: what falls outside it is clipped. That is what
|
||
|
|
// makes tiled rendering work -- give each tile the transform that selects it,
|
||
|
|
//
|
||
|
|
// opts.ctm = matrix_mul(scale_matrix(4, 4), translate_matrix(-tx, -ty))
|
||
|
|
//
|
||
|
|
// and each call fills one tile.
|
||
|
|
render_page_to :: proc(page: ^Page, bmp: ^Bitmap, opts := Render_Options{}) -> Error {
|
||
|
|
if page == nil || page.doc == nil || bmp == nil {
|
||
|
|
return .Invalid_Argument
|
||
|
|
}
|
||
|
|
if bmp.width <= 0 || bmp.height <= 0 || len(bmp.pixels) == 0 || bmp.n < 1 || bmp.n > 4 {
|
||
|
|
return .Invalid_Argument
|
||
|
|
}
|
||
|
|
ctm := page_ctm_or_identity(opts.ctm)
|
||
|
|
if !matrix_is_finite(ctm) {
|
||
|
|
return .Invalid_Argument
|
||
|
|
}
|
||
|
|
|
||
|
|
page_fill_background(bmp, page_background(opts))
|
||
|
|
|
||
|
|
scratch: Page_Scratch
|
||
|
|
if serr := page_scratch_begin(&scratch); serr != .None {
|
||
|
|
// The background is painted, so the bitmap is a usable blank page; it
|
||
|
|
// must still leave here with straight alpha like every other path.
|
||
|
|
bitmap_unpremultiply(bmp)
|
||
|
|
return serr
|
||
|
|
}
|
||
|
|
defer page_scratch_end(&scratch)
|
||
|
|
|
||
|
|
rd, derr := raster_device_make(bmp, Raster_Clip{0, 0, bmp.width, bmp.height},
|
||
|
|
context.allocator, page.doc)
|
||
|
|
if derr != .None || rd == nil {
|
||
|
|
// The background is painted and the bitmap is valid; only the drawing
|
||
|
|
// is missing.
|
||
|
|
bitmap_unpremultiply(bmp)
|
||
|
|
return derr == .None ? .Out_Of_Memory : derr
|
||
|
|
}
|
||
|
|
defer raster_device_destroy(rd)
|
||
|
|
|
||
|
|
err := page_run(page, raster_device_as_device(rd), ctm, &scratch, !opts.no_annotations, false)
|
||
|
|
|
||
|
|
// Everything below the Device line works in premultiplied alpha; a caller
|
||
|
|
// uploading this to a texture wants it straight (bitmap.odin says so).
|
||
|
|
bitmap_unpremultiply(bmp)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// page_background resolves the options' background to an RGBA colour.
|
||
|
|
@(private)
|
||
|
|
page_background :: proc(opts: Render_Options) -> [4]u8 {
|
||
|
|
bg := opts.background
|
||
|
|
if bg == ([4]u8{0, 0, 0, 0}) {
|
||
|
|
// Unset, which is what a zeroed Render_Options carries. An opaque page
|
||
|
|
// defaults to white; an alpha page defaults to nothing at all.
|
||
|
|
return opts.alpha ? [4]u8{0, 0, 0, 0} : [4]u8{255, 255, 255, 255}
|
||
|
|
}
|
||
|
|
if !opts.alpha {
|
||
|
|
bg[3] = 255
|
||
|
|
}
|
||
|
|
return bg
|
||
|
|
}
|
||
|
|
|
||
|
|
// page_fill_background paints `rgba` and leaves the bitmap PREMULTIPLIED,
|
||
|
|
// which is the state every bitmap below the Device line is in.
|
||
|
|
//
|
||
|
|
// The colour goes through `blend_source_for` and `blend_quantize` rather than
|
||
|
|
// being reduced here, so the background of a greyscale target is the same grey
|
||
|
|
// a content stream painting that colour into the same bitmap would produce.
|
||
|
|
// Two answers to "how bright is this colour" is exactly the kind of seam this
|
||
|
|
// library has had to fix once already.
|
||
|
|
@(private)
|
||
|
|
page_fill_background :: proc(bmp: ^Bitmap, rgba: [4]u8) {
|
||
|
|
a := f64(rgba[3]) / 255
|
||
|
|
src := blend_source_for(bmp.n, [3]f64{
|
||
|
|
f64(rgba[0]) / 255, f64(rgba[1]) / 255, f64(rgba[2]) / 255,
|
||
|
|
})
|
||
|
|
c0 := blend_quantize(src[0] * a)
|
||
|
|
c1 := blend_quantize(src[1] * a)
|
||
|
|
c2 := blend_quantize(src[2] * a)
|
||
|
|
|
||
|
|
fill: [4]u8
|
||
|
|
switch bmp.n {
|
||
|
|
case 1: fill = {c0, 0, 0, 0}
|
||
|
|
case 2: fill = {c0, rgba[3], 0, 0}
|
||
|
|
case 3: fill = {c0, c1, c2, 0}
|
||
|
|
case: fill = {c0, c1, c2, rgba[3]}
|
||
|
|
}
|
||
|
|
bitmap_fill(bmp, fill)
|
||
|
|
bmp.premultiplied = true
|
||
|
|
}
|
||
|
|
|
||
|
|
// page_ctm_or_identity reads a caller's transform. The zero value of a Matrix is not a
|
||
|
|
// transform at all -- it collapses the page to a point -- so it is read as
|
||
|
|
// MATRIX_IDENTITY, which makes a zeroed Render_Options mean "no extra
|
||
|
|
// transform" rather than "draw nothing". It is named for this file rather
|
||
|
|
// than for Matrix because it is a convention of this API, not of the geometry.
|
||
|
|
@(private)
|
||
|
|
page_ctm_or_identity :: proc "contextless" (m: Matrix) -> Matrix {
|
||
|
|
zero: Matrix
|
||
|
|
return m == zero ? MATRIX_IDENTITY : m
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Text extraction
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// extract_text returns the page's text, reconstructed in reading order.
|
||
|
|
//
|
||
|
|
// It is the same interpreter run `render_page` performs, into a Text_Device
|
||
|
|
// instead of a rasteriser, which is why the text a page extracts to always
|
||
|
|
// agrees with the text it draws: invisible OCR text under a scan comes out,
|
||
|
|
// text drawn as paths does not, and a glyph's position is the position it
|
||
|
|
// would have been painted at. Spans are joined with nothing, lines with a
|
||
|
|
// newline and blocks with a blank line, and there is no trailing newline.
|
||
|
|
//
|
||
|
|
// Annotation appearance streams are included: the value a form field shows is
|
||
|
|
// text on the page as far as a reader is concerned. Optional content that the
|
||
|
|
// catalog's default configuration hides is included too -- hidden text is
|
||
|
|
// still text.
|
||
|
|
//
|
||
|
|
// THE STRING BELONGS TO THE CALLER: it is allocated with `allocator` and is
|
||
|
|
// released with `delete(s, allocator)`.
|
||
|
|
extract_text :: proc(page: ^Page, allocator := context.allocator) -> (string, Error) {
|
||
|
|
if page == nil || page.doc == nil {
|
||
|
|
return "", .Invalid_Argument
|
||
|
|
}
|
||
|
|
|
||
|
|
td, terr := text_device_make(context.allocator)
|
||
|
|
if terr != .None || td == nil {
|
||
|
|
return "", terr == .None ? .Out_Of_Memory : terr
|
||
|
|
}
|
||
|
|
defer text_device_destroy(td)
|
||
|
|
|
||
|
|
scratch: Page_Scratch
|
||
|
|
if serr := page_scratch_begin(&scratch); serr != .None {
|
||
|
|
return "", serr
|
||
|
|
}
|
||
|
|
defer page_scratch_end(&scratch)
|
||
|
|
|
||
|
|
// The base matrix is the same one render_page uses, so the device sees
|
||
|
|
// glyphs where a reader would see them: x to the right, y down the page.
|
||
|
|
// Reading order falls out of that and must not be re-derived here.
|
||
|
|
err := page_run(page, text_device_as_device(td), MATRIX_IDENTITY, &scratch, true, true)
|
||
|
|
|
||
|
|
return text_device_result(td, allocator), err
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Display lists
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// page_display_list records the page's drawing calls instead of performing
|
||
|
|
// them, for a caller with a backend of their own: replaying the list into a
|
||
|
|
// device produces what running the page into that device directly would.
|
||
|
|
//
|
||
|
|
// `ctm` is applied on top of the page flip and /Rotate, exactly as everywhere
|
||
|
|
// else here, so a list recorded with MATRIX_IDENTITY is in the same device
|
||
|
|
// space `render_page` rasterises into and `display_list_bounds` can be
|
||
|
|
// compared with `page_size`. Annotation appearances are recorded too, the way
|
||
|
|
// `render_page` draws them: the list is the whole page, not just its content
|
||
|
|
// stream.
|
||
|
|
//
|
||
|
|
// THE LIST BELONGS TO THE CALLER. Everything in it, and the Display_List
|
||
|
|
// itself, comes from `allocator`:
|
||
|
|
//
|
||
|
|
// dl, _ := pdf.page_display_list(&page, pdf.MATRIX_IDENTITY)
|
||
|
|
// defer { pdf.display_list_destroy(dl); free(dl) }
|
||
|
|
//
|
||
|
|
// The Fonts, Images, Shadings and Colorspaces it points at belong to the
|
||
|
|
// Document and stay valid for as long as it is open.
|
||
|
|
page_display_list :: proc(page: ^Page, ctm: Matrix, allocator := context.allocator) -> (^Display_List, Error) {
|
||
|
|
if page == nil || page.doc == nil {
|
||
|
|
return nil, .Invalid_Argument
|
||
|
|
}
|
||
|
|
m := page_ctm_or_identity(ctm)
|
||
|
|
if !matrix_is_finite(m) {
|
||
|
|
return nil, .Invalid_Argument
|
||
|
|
}
|
||
|
|
|
||
|
|
ld, lerr := list_device_make(allocator)
|
||
|
|
if lerr != .None || ld == nil {
|
||
|
|
return nil, lerr == .None ? .Out_Of_Memory : lerr
|
||
|
|
}
|
||
|
|
|
||
|
|
dl, aerr := new(Display_List, allocator)
|
||
|
|
if aerr != nil || dl == nil {
|
||
|
|
list_device_destroy(ld)
|
||
|
|
return nil, .Out_Of_Memory
|
||
|
|
}
|
||
|
|
|
||
|
|
scratch: Page_Scratch
|
||
|
|
if serr := page_scratch_begin(&scratch); serr != .None {
|
||
|
|
free(dl, allocator)
|
||
|
|
list_device_destroy(ld)
|
||
|
|
return nil, serr
|
||
|
|
}
|
||
|
|
defer page_scratch_end(&scratch)
|
||
|
|
|
||
|
|
err := page_run(page, list_device_as_device(ld), m, &scratch, true, false)
|
||
|
|
|
||
|
|
// The list is moved out of the device, which is then torn down: the
|
||
|
|
// recorded commands own their copies and outlive the recorder. Zeroing the
|
||
|
|
// device's copy is what stops `list_device_destroy` freeing them.
|
||
|
|
dl^ = ld.list
|
||
|
|
ld.list = {}
|
||
|
|
list_device_destroy(ld)
|
||
|
|
|
||
|
|
return dl, err
|
||
|
|
}
|