Files
pdf-odin-llm/examples/pdftext/main.odin
T
2026-09-25 23:32:29 +02:00

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
}