initial commit
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
// 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 <file.pdf> [-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]
|
||||
}
|
||||
Reference in New Issue
Block a user