// pdfrender rasterises a page and writes it out as a binary PPM (P6) or PNG. // // It is the smallest program that exercises the whole library: open, page // tree, content interpreter, every device below it, and back out as pixels. // Like pdfinfo it also keeps the public surface honest -- it is compiled by // scripts/check.sh, so a procedure a renderer needs that is accidentally // @(private) breaks the build here rather than in a consumer's project. // // odin run examples/pdfrender -- file.pdf # page 1 -> out.ppm // odin run examples/pdfrender -- file.pdf -page=7 -scale=2 -o=seven.png // // Options: // -page=N 1-based page number (default 1) // -scale=F device pixels per point (default 1) // -o=PATH output file; .png writes a PNG, anything else a PPM // -no-annots draw the content stream only // -gray report ink coverage instead of writing a file package main import "core:fmt" import "core:hash" import "core:os" import "core:strconv" import "core:strings" import pdf "../.." Options :: struct { path: string, out: string, page: int, scale: f64, no_annots: bool, stat_only: bool, } main :: proc() { opts, ok := parse_args() if !ok { fmt.eprintfln("usage: %s [-page=N] [-scale=F] [-o=out.ppm|out.png] [-no-annots] [-gray]", len(os.args) > 0 ? os.args[0] : "pdfrender") os.exit(2) } if !render(opts) { os.exit(1) } } parse_args :: proc() -> (opts: Options, ok: bool) { opts = Options{page = 1, scale = 1, out = "out.ppm"} for arg in os.args[1:] { switch { case strings.has_prefix(arg, "-page="): n, nok := strconv.parse_int(arg[len("-page="):]) if !nok || n < 1 { return opts, false } opts.page = n case strings.has_prefix(arg, "-scale="): f, fok := strconv.parse_f64(arg[len("-scale="):]) if !fok || f <= 0 { return opts, false } opts.scale = f case strings.has_prefix(arg, "-o="): opts.out = arg[len("-o="):] case arg == "-no-annots": opts.no_annots = true case arg == "-gray": opts.stat_only = true case strings.has_prefix(arg, "-"): return opts, false case: if opts.path != "" { return opts, false } opts.path = arg } } return opts, opts.path != "" } render :: proc(opts: Options) -> bool { doc, err := pdf.open_file(opts.path) if err != .None { fmt.eprintfln("%s: %s", opts.path, pdf.error_string(err)) return false } defer pdf.close(doc) if opts.page > pdf.page_count(doc) { fmt.eprintfln("%s: page %d of %d", opts.path, opts.page, pdf.page_count(doc)) return false } page, perr := pdf.page_get(doc, opts.page - 1) if perr != .None { fmt.eprintfln("%s: page %d: %s", opts.path, opts.page, pdf.error_string(perr)) return false } // The scale rides on top of the page flip and /Rotate, which // page_base_matrix applies for us; the bitmap comes out page_size big. bmp, rerr := pdf.render_page(&page, { ctm = pdf.scale_matrix(opts.scale, opts.scale), no_annotations = opts.no_annots, }) defer pdf.bitmap_destroy(&bmp) // A render that reports an error still hands back everything it managed to // draw, so say so and write the pixels anyway rather than throwing them // away -- that is the difference between "damaged" and "unreadable". if rerr != .None { fmt.eprintfln("%s: page %d rendered with %s", opts.path, opts.page, pdf.error_string(rerr)) if bmp.pixels == nil { return false } } ink := ink_fraction(&bmp) fmt.printfln("%s page %d: %dx%d px, %.2f%% ink", opts.path, opts.page, bmp.width, bmp.height, ink * 100) if opts.stat_only { return true } data: []byte defer delete(data) if strings.has_suffix(strings.to_lower(opts.out, context.temp_allocator), ".png") { data = encode_png(&bmp) } else { data = encode_ppm(&bmp) } if data == nil { fmt.eprintfln("%s: could not encode the image", opts.out) return false } if werr := os.write_entire_file(opts.out, data); werr != nil { fmt.eprintfln("%s: %v", opts.out, werr) return false } fmt.printfln("wrote %s (%d bytes)", opts.out, len(data)) return true } // ink_fraction is the share of pixels that are not the untouched background. // A page that renders pure white is nearly always a bug, so this is the one // number worth printing without opening the image. ink_fraction :: proc(bmp: ^pdf.Bitmap) -> f64 { if bmp.width <= 0 || bmp.height <= 0 { return 0 } inked := 0 for y in 0 ..< bmp.height { row := pdf.bitmap_row(bmp, y) for x in 0 ..< bmp.width { p := row[x * bmp.n:] if p[0] != 255 || (bmp.n >= 3 && (p[1] != 255 || p[2] != 255)) { inked += 1 } } } return f64(inked) / f64(bmp.width * bmp.height) } // --------------------------------------------------------------------------- // Writers // // PPM because it is four lines and every image tool reads it; PNG because // nothing else does without one. Both drop alpha: render_page starts the page // opaque white unless asked otherwise, so there is none to keep. // --------------------------------------------------------------------------- encode_ppm :: proc(bmp: ^pdf.Bitmap) -> []byte { b: strings.Builder strings.builder_init(&b) fmt.sbprintf(&b, "P6\n%d %d\n255\n", bmp.width, bmp.height) for y in 0 ..< bmp.height { row := pdf.bitmap_row(bmp, y) for x in 0 ..< bmp.width { r, g, bl := pixel_rgb(bmp, row, x) strings.write_byte(&b, r) strings.write_byte(&b, g) strings.write_byte(&b, bl) } } return b.buf[:] } encode_png :: proc(bmp: ^pdf.Bitmap) -> []byte { raw: [dynamic]byte defer delete(raw) for y in 0 ..< bmp.height { row := pdf.bitmap_row(bmp, y) append(&raw, 0) // filter type 0: none for x in 0 ..< bmp.width { r, g, bl := pixel_rgb(bmp, row, x) append(&raw, r, g, bl) } } out: [dynamic]byte append(&out, 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a) ihdr: [13]byte put_u32(ihdr[0:], u32(bmp.width)) put_u32(ihdr[4:], u32(bmp.height)) ihdr[8] = 8 // bit depth ihdr[9] = 2 // colour type 2: truecolour png_chunk(&out, "IHDR", ihdr[:]) idat := zlib_stored(raw[:]) defer delete(idat) png_chunk(&out, "IDAT", idat) png_chunk(&out, "IEND", nil) return out[:] } // zlib_stored wraps bytes in a zlib stream of uncompressed deflate blocks. // // core:compress has an inflater and no deflater, and a PNG's IDAT must be a // zlib stream whatever is inside it -- so this is the honest way to write one // without a compressor: stored blocks are a legal encoding that every decoder // accepts. The file is the size of the pixels; PPM is the better choice if // that matters, and this exists so the example can hand over something a // browser opens. zlib_stored :: proc(data: []byte) -> []byte { MAX_BLOCK :: 65535 out: [dynamic]byte append(&out, 0x78, 0x01) // CM=8, CINFO=7, FCHECK making it a multiple of 31 remaining := data for { n := min(len(remaining), MAX_BLOCK) last: byte = len(remaining) - n == 0 ? 1 : 0 append(&out, last, byte(n), byte(n >> 8), byte(~u16(n)), byte(~u16(n) >> 8)) append(&out, ..remaining[:n]) remaining = remaining[n:] if last == 1 { break } } sum := hash.adler32(data) m := len(out) resize(&out, m + 4) put_u32(out[m:], sum) return out[:] } png_chunk :: proc(out: ^[dynamic]byte, tag: string, data: []byte) { n := len(out) resize(out, n + 4) put_u32(out[n:], u32(len(data))) append(out, tag) append(out, ..data) // The CRC covers the tag and the data but not the length. crc := hash.crc32(out[n + 4:]) m := len(out) resize(out, m + 4) put_u32(out[m:], crc) } put_u32 :: proc(dst: []byte, v: u32) { dst[0] = byte(v >> 24) dst[1] = byte(v >> 16) dst[2] = byte(v >> 8) dst[3] = byte(v) } // A rendered page is RGBA8, but render_page_to accepts any component count, so // handle the grey and RGB cases rather than assuming. pixel_rgb :: proc(bmp: ^pdf.Bitmap, row: []byte, x: int) -> (r, g, b: byte) { p := row[x * bmp.n:] if bmp.n >= 3 { return p[0], p[1], p[2] } return p[0], p[0], p[0] }