120 lines
3.7 KiB
Odin
120 lines
3.7 KiB
Odin
// 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 <file.pdf> [-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
|
|
}
|