// pdftext prints the text of a page, or of a whole document. // // The text comes off the same interpreter that draws the page, so what it // prints is what a renderer would have painted: invisible OCR text under a // scan comes out, text drawn as vector paths does not, and the reading order // is the order the glyphs were placed in. Like the other examples it is // compiled by scripts/check.sh, which is what keeps the public API honest. // // odin run examples/pdftext -- file.pdf # page 1 // odin run examples/pdftext -- file.pdf -page=all # every page // odin run examples/pdftext -- file.pdf -page=3 -stats // // Options: // -page=N|all 1-based page number, or every page (default 1) // -stats print per-page character counts instead of the text package main import "core:fmt" import "core:os" import "core:strconv" import "core:strings" import "core:unicode/utf8" import pdf "../.." main :: proc() { path, page, stats, ok := parse_args() if !ok { fmt.eprintfln("usage: %s [-page=N|all] [-stats]", len(os.args) > 0 ? os.args[0] : "pdftext") os.exit(2) } doc, err := pdf.open_file(path) if err != .None { fmt.eprintfln("%s: %s", path, pdf.error_string(err)) os.exit(1) } defer pdf.close(doc) n := pdf.page_count(doc) first, last := page, page if page == ALL_PAGES { first, last = 0, n - 1 } if first < 0 || last >= n { fmt.eprintfln("%s: page %d of %d", path, first + 1, n) os.exit(1) } total, with_text, replacements := 0, 0, 0 for i in first ..= last { p, perr := pdf.page_get(doc, i) if perr != .None { fmt.eprintfln("page %d: %s", i + 1, pdf.error_string(perr)) continue } // extract_text hands back a string the caller owns. text, terr := pdf.extract_text(&p) defer delete(text) if terr != .None { fmt.eprintfln("page %d: %s", i + 1, pdf.error_string(terr)) } total += len(text) if len(text) > 0 { with_text += 1 } replacements += count_replacements(text) if stats { fmt.printfln("page %d: %d bytes, %d runes, %d U+FFFD", i + 1, len(text), utf8.rune_count(text), count_replacements(text)) continue } if first != last { fmt.printfln("--- page %d ---", i + 1) } fmt.println(text) } if stats || first != last { fmt.printfln("%d of %d pages yielded text, %d bytes, %d U+FFFD", with_text, last - first + 1, total, replacements) } } ALL_PAGES :: -1 parse_args :: proc() -> (path: string, page: int, stats: bool, ok: bool) { for arg in os.args[1:] { switch { case arg == "-page=all": page = ALL_PAGES case strings.has_prefix(arg, "-page="): n, nok := strconv.parse_int(arg[len("-page="):]) if !nok || n < 1 { return "", 0, false, false } page = n - 1 case arg == "-stats": stats = true case strings.has_prefix(arg, "-"): return "", 0, false, false case: if path != "" { return "", 0, false, false } path = arg } } return path, page, stats, path != "" } // A code the font could not map to Unicode extracts as U+FFFD, so counting // them is the cheapest measure of how well the /ToUnicode and encoding chain // did on a file. A page that is mostly replacement characters has text on it // that nothing can search or copy. count_replacements :: proc(s: string) -> int { n := 0 for r in s { if r == utf8.RUNE_ERROR { n += 1 } } return n }