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 }