Skip to content

Scoped Extraction — Pull Content from a Specific Region

When you’re processing invoices, bank statements, tax forms, or any templated layout, you usually know where the fields live. Instead of extracting the whole page and searching for the value, point PDF Oxide at the exact rectangle and get back just what’s there.

The fluent within(page, rect) API returns a scoped region you can chain extraction methods on: extract_text(), extract_words(), extract_chars(), extract_tables().

Binding coverage. within(page, rect) is available in Python, Rust, and WASM. Go and C# expose the equivalent lower-level helpers (ExtractTextInRect, ExtractWordsInRect, ExtractImagesInRect) — see below. The full in-rect family (text, words, lines, tables, images) is exposed end-to-end in Rust, the C ABI, and the Swift wrapper; see In-rect extraction variants for which binding has which.

Quick Example

rect is (x, y, width, height) in PDF points, with the origin at the bottom-left of the page. Letter-size pages are 612 × 792 points.

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")

# Top 92 points of page 0 — typical header band
header = doc.within(0, (0, 700, 612, 92)).extract_text()
print(header)

Rust

use pdf_oxide::PdfDocument;
use pdf_oxide::geometry::Rect;

let mut doc = PdfDocument::open("invoice.pdf")?;
let header = doc.within(0, Rect::new(0.0, 700.0, 612.0, 92.0)).extract_text()?;
println!("{}", header);

JavaScript (WASM)

import { WasmPdfDocument } from "pdf-oxide-wasm";

const doc = new WasmPdfDocument(bytes);
const headerRegion = doc.within(0, [0, 700, 612, 92]);
console.log(headerRegion.extractText());
doc.free();

Go (lower-level helper, same effect)

package main

import (
    "fmt"
    "log"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    doc, err := pdfoxide.Open("invoice.pdf")
    if err != nil { log.Fatal(err) }
    defer doc.Close()

    // ExtractTextInRect(pageIndex, x, y, width, height)
    header, _ := doc.ExtractTextInRect(0, 0, 700, 612, 92)
    fmt.Println(header)
}

C# (lower-level helper)

using PdfOxide;

using var doc = PdfDocument.Open("invoice.pdf");
string header = doc.ExtractTextInRect(0, 0, 700, 612, 92);
Console.WriteLine(header);

Java (page.text(region); BBox is corner-form (x0, y0, x1, y1))

import fyi.oxide.pdf.PdfDocument;
import fyi.oxide.pdf.geometry.BBox;

try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("invoice.pdf"))) {
    // Top 92 points of page 0 → corners (0, 700) … (612, 792)
    String header = doc.page(0).text(new BBox(0, 700, 612, 792));
    System.out.println(header);
}

Kotlin

import fyi.oxide.pdf.PdfDocument
import fyi.oxide.pdf.geometry.BBox

PdfDocument.open(java.nio.file.Path.of("invoice.pdf")).use { doc ->
    val header = doc.page(0).text(BBox(0.0, 700.0, 612.0, 792.0))
    println(header)
}

Scala

import fyi.oxide.pdf.PdfDocument
import fyi.oxide.pdf.geometry.BBox
import scala.util.Using

Using.resource(PdfDocument.open("invoice.pdf")) { doc =>
  val header = doc.page(0).text(BBox(0, 700, 612, 792))
  println(header)
}

Clojure

(require '[pdf-oxide.core :as pdf])
(import '[fyi.oxide.pdf.geometry BBox])

(with-open [doc (pdf/open "invoice.pdf")]
  ;; Top 92 points of page 0 → corners (0 700) … (612 792)
  (println (pdf/page-text (pdf/page doc 0) (BBox. 0 700 612 792))))

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("invoice.pdf");
// extract_text_in_rect(page, x, y, w, h)
auto header = doc.extract_text_in_rect(0, 0, 700, 612, 92);
std::cout << header << "\n";

Swift

import PdfOxide

let doc = try Document.open("invoice.pdf")
let header = try doc.extractTextInRect(0, x: 0, y: 700, w: 612, h: 92)
print(header)

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('invoice.pdf');
final header = doc.extractTextInRect(0, 0, 700, 612, 92);
print(header);
doc.close();

R

library(pdfoxide)

doc <- pdf_open("invoice.pdf")
# pdf_extract_text_in_rect(doc, page, x, y, width, height)
header <- pdf_extract_text_in_rect(doc, 0, 0, 700, 612, 92)
cat(header)

Julia

using PdfOxide

doc = open_document("invoice.pdf")
header = extract_text_in_rect(doc, 0, 0, 700, 612, 92)
println(header)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("invoice.pdf");
const header = try doc.extractTextInRect(a, 0, 0, 700, 612, 92);  // free header
std.debug.print("{s}\n", .{header});

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"invoice.pdf" error:&err];
NSString *header = [doc extractTextInRect:0 x:0 y:700 w:612 h:92 error:&err];
NSLog(@"%@", header);

Elixir

{:ok, doc} = PdfOxide.open("invoice.pdf")
# extract_text_in_rect(doc, page, x, y, w, h)
{:ok, header} = PdfOxide.extract_text_in_rect(doc, 0, 0, 700, 612, 92)
IO.puts(header)

Chained extraction from a region

The within() fluent form in Python / Rust / WASM lets you call any extraction method on the same scoped region without re-specifying the rect:

Python

doc = PdfDocument("invoice.pdf")
region = doc.within(0, (400, 100, 200, 200))   # bottom-right 200×200 box

total_text = region.extract_text()              # plain text
words      = region.extract_words()             # word-level records
chars      = region.extract_chars()             # character-level records

Rust

let region = doc.within(0, Rect::new(400.0, 100.0, 200.0, 200.0));
let text  = region.extract_text()?;
let words = region.extract_words()?;

C++ (no fluent chain — call each in-rect helper against the same rect)

// bottom-right 200×200 box: x=400, y=100, w=200, h=200
auto text  = doc.extract_text_in_rect(0, 400, 100, 200, 200);
auto words = doc.extract_words_in_rect(0, 400, 100, 200, 200);
auto lines = doc.extract_lines_in_rect(0, 400, 100, 200, 200);

Swift

let text  = try doc.extractTextInRect(0, x: 400, y: 100, w: 200, h: 200)
let words = try doc.extractWordsInRect(0, x: 400, y: 100, w: 200, h: 200)

Dart

final text  = doc.extractTextInRect(0, 400, 100, 200, 200);
final words = doc.extractWordsInRect(0, 400, 100, 200, 200);

R

text  <- pdf_extract_text_in_rect(doc, 0, 400, 100, 200, 200)
words <- pdf_extract_words_in_rect(doc, 0, 400, 100, 200, 200)

Julia

text  = extract_text_in_rect(doc, 0, 400, 100, 200, 200)
words = extract_words_in_rect(doc, 0, 400, 100, 200, 200)

Zig

const text  = try doc.extractTextInRect(a, 0, 400, 100, 200, 200);
const words = try doc.extractWordsInRect(a, 0, 400, 100, 200, 200);  // freeWords

Objective-C

NSString *text = [doc extractTextInRect:0 x:400 y:100 w:200 h:200 error:&err];
NSArray<POXWord*> *words = [doc extractWordsInRect:0 x:400 y:100 w:200 h:200 error:&err];

Elixir

{:ok, text}  = PdfOxide.extract_text_in_rect(doc, 0, 400, 100, 200, 200)
{:ok, words} = PdfOxide.extract_words_in_rect(doc, 0, 400, 100, 200, 200)

Common use cases

Invoice field extraction

An invoice usually has the vendor address, invoice number, and line-item table in fixed zones. Define the rects once per template:

from pdf_oxide import PdfDocument

TEMPLATES = {
    "acme_v1": {
        "invoice_no":  (450, 720,  120,  20),
        "issue_date":  (450, 700,  120,  20),
        "vendor_name": ( 50, 740,  300,  40),
        "total":       (450, 100,  120,  24),
    },
}

def parse_invoice(path, template):
    doc = PdfDocument(path)
    out = {}
    for field, rect in template.items():
        out[field] = doc.within(0, rect).extract_text().strip()
    return out

print(parse_invoice("invoice-2025-04.pdf", TEMPLATES["acme_v1"]))

Bank statement line items

Most statements have a narrow “transactions” band. Crop to that band and call extract_words() to get each line in reading order with its bbox:

doc = PdfDocument("statement.pdf")
for page in range(doc.page_count()):
    txn_region = doc.within(page, (36, 72, 540, 650))   # skip header + footer
    for w in txn_region.extract_words():
        print(f"page {page}: {w.text} at ({w.x0:.0f},{w.y0:.0f})")

If you’re indexing the body content only, crop away the top and bottom of every page:

Rust

let mut doc = PdfDocument::open("book.pdf")?;
for i in 0..doc.page_count()? {
    let body = doc.within(i, Rect::new(0.0, 100.0, 612.0, 600.0))
                  .extract_text()?;
    // index `body` …
}

Table region detection

When you already know a page contains a table and where, scope to the table rect and let extract_tables() focus only on that region:

Python

tables = doc.within(0, (50, 200, 500, 400)).extract_tables()
for t in tables:
    for row in t["rows"]:
        print([c["text"] for c in row["cells"]])

What rect-scoped extraction variants exist?

Beyond extract_text(), extract_words(), and extract_chars(), two more rect-scoped variants give you geometry-aware results from a single rectangle: lines in rect and tables in rect. Both filter a full-page extraction down to the regions whose bounding box intersects your rectangle, so the coordinates and reading order you get back are the same as a full-page call — just cropped.

Extract text lines in a region (extract_lines_in_rect)

Returns the line-level records (each with its text, bounding box, and word count) that fall inside the rectangle. Use it when you need whole lines in reading order rather than individual words — e.g. address blocks, multi-line totals, or a single statement row.

The C ABI signature is authoritative:

FfiTextLineList *pdf_document_extract_lines_in_rect(
    PdfDocument *handle,
    int32_t page_index,
    float x, float y, float w, float h,
    int32_t *error_code);

Rustextract_lines_in_rect(page_index, region) -> Result<Vec<PathContent>> on PdfDocument:

use pdf_oxide::PdfDocument;
use pdf_oxide::geometry::Rect;

let doc = PdfDocument::open("statement.pdf")?;

// Transactions band: skip the header (top 92pt) and footer (bottom 72pt)
let region = Rect::new(36.0, 72.0, 540.0, 628.0);
let lines = doc.extract_lines_in_rect(0, region)?;
for line in &lines {
    println!("{:?}", line.bbox);
}

Python — the fluent region exposes lines via extract_text_lines():

from pdf_oxide import PdfDocument

doc = PdfDocument("statement.pdf")

# Same band as the Rust example above
region = doc.within(0, (36, 72, 540, 628))
for line in region.extract_text_lines():
    print(line.text, line.bbox)

SwiftextractLinesInRect(_:x:y:w:h:) returns [TextLine]:

import PdfOxide

let doc = try PdfDocument(path: "statement.pdf")
let lines = try doc.extractLinesInRect(0, x: 36, y: 72, w: 540, h: 628)
for line in lines {
    print(line.text, line.bbox, line.wordCount)
}

C++extract_lines_in_rect(page, x, y, w, h) returns std::vector<TextLine>:

auto lines = doc.extract_lines_in_rect(0, 36, 72, 540, 628);
for (const auto& line : lines) {
    std::cout << line.text << "\n";
}

DartextractLinesInRect(page, x, y, w, h) returns List<TextLine>:

final lines = doc.extractLinesInRect(0, 36, 72, 540, 628);
for (final line in lines) {
    print('${line.text} ${line.bbox}');
}

Rpdf_extract_lines_in_rect(doc, page, x, y, width, height):

lines <- pdf_extract_lines_in_rect(doc, 0, 36, 72, 540, 628)

Juliaextract_lines_in_rect(doc, page, x, y, w, h):

lines = extract_lines_in_rect(doc, 0, 36, 72, 540, 628)
for line in lines
    println(line.text, " ", line.bbox)
end

ZigextractLinesInRect(allocator, page, x, y, w, h):

const lines = try doc.extractLinesInRect(a, 0, 36, 72, 540, 628);  // freeTextLines

Objective-CextractLinesInRect:x:y:w:h: returns NSArray<POXTextLine*>:

NSArray<POXTextLine*> *lines = [doc extractLinesInRect:0 x:36 y:72 w:540 h:628 error:&err];

Elixirextract_lines_in_rect(doc, page, x, y, w, h):

{:ok, lines} = PdfOxide.extract_lines_in_rect(doc, 0, 36, 72, 540, 628)

Go / C#. The extract_lines_in_rect C entry point exists, but the Go and C# wrappers do not yet surface it. In those languages, extract lines for the whole page and filter on the returned bounding boxes, or use ExtractWordsInRect (Go) and group words into lines yourself.

Extract tables in a region (extract_tables_in_rect)

Scopes table detection to a single rectangle — only tables whose bounding box intersects the rect come back. This is the geometry-aware counterpart to the fluent within(...).extract_tables() shown above.

C ABI signature:

FfiTableList *pdf_document_extract_tables_in_rect(
    PdfDocument *handle,
    int32_t page_index,
    float x, float y, float w, float h,
    int32_t *error_code);

Rustextract_tables_in_rect(page_index, region) -> Result<Vec<Table>> (a ..._with_config variant takes a custom TableDetectionConfig):

use pdf_oxide::PdfDocument;
use pdf_oxide::geometry::Rect;

let doc = PdfDocument::open("invoice.pdf")?;
let region = Rect::new(50.0, 200.0, 500.0, 400.0);
let tables = doc.extract_tables_in_rect(0, region)?;
for table in &tables {
    println!("{} rows × {} cols", table.rows.len(), table.col_count);
}

Python — via the fluent region:

from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
tables = doc.within(0, (50, 200, 500, 400)).extract_tables()
for t in tables:
    for row in t["rows"]:
        print([c["text"] for c in row["cells"]])

SwiftextractTablesInRect(_:x:y:w:h:) returns [Table]:

let tables = try doc.extractTablesInRect(0, x: 50, y: 200, w: 500, h: 400)
for table in tables {
    print("\(table.rowCount) rows, header: \(table.hasHeader)")
}

C++extract_tables_in_rect(page, x, y, w, h) returns std::vector<Table>:

auto tables = doc.extract_tables_in_rect(0, 50, 200, 500, 400);
for (const auto& table : tables) {
    std::cout << table.rows.size() << " rows\n";
}

DartextractTablesInRect(page, x, y, w, h) returns List<Table>:

final tables = doc.extractTablesInRect(0, 50, 200, 500, 400);
for (final table in tables) {
    print('${table.rows.length} rows');
}

Rpdf_extract_tables_in_rect(doc, page, x, y, width, height):

tables <- pdf_extract_tables_in_rect(doc, 0, 50, 200, 500, 400)

Juliaextract_tables_in_rect(doc, page, x, y, w, h):

tables = extract_tables_in_rect(doc, 0, 50, 200, 500, 400)

ZigextractTablesInRect(allocator, page, x, y, w, h):

const tables = try doc.extractTablesInRect(a, 0, 50, 200, 500, 400);

Objective-CextractTablesInRect:x:y:w:h: returns NSArray<POXTable*>:

NSArray<POXTable*> *tables = [doc extractTablesInRect:0 x:50 y:200 w:500 h:400 error:&err];

Elixirextract_tables_in_rect(doc, page, x, y, w, h):

{:ok, tables} = PdfOxide.extract_tables_in_rect(doc, 0, 50, 200, 500, 400)

Go / C#. As with lines, the extract_tables_in_rect C entry point exists but is not yet wrapped in Go or C#. Call ExtractTables(page) for the whole page and keep the tables whose bbox falls inside your rect.

How do I auto-extract a page without choosing text vs OCR?

When you don’t know whether a page is born-digital text, a scan, or a mix, extract_page_auto does the routing for you. It runs the AutoExtractor — per-region text-vs-OCR routing with graceful native fallback (it never surfaces an opaque OCR error) — and returns a JSON PageExtraction: a page kind, the assembled reading-ordered text, a confidence, a typed reason, an ocr_used flag, and a regions[] array where every region carries a bbox, kind, text, confidence, source, and reason (the bbox + reason are present even when a region’s text is empty, so reading order is never silently corrupted).

It is {}-tolerant: pass an empty/null options JSON for the defaults, or supply an AutoExtractOptions object. The recognized fields (serialized snake_case) are:

Field Type Default Meaning
mode "text_only" | "auto" | "force_ocr" "auto" Text-vs-OCR routing strategy
reconstruct_image_tables bool true Rebuild image-only tables via the spatial detector over OCR spans
emit_placeholders bool true Emit positioned Figure/Table placeholders in the text flow
ocr_languages string[] [] OCR language hints (e.g. ["english","chinese"])
min_text_confidence float | null null Auto-decision confidence threshold
table_confidence float | null null Image-table reconstruction threshold
force_ocr_pages int[] [] 0-based page indices to force OCR on

OCR feature gate. OCR only actually runs when the library is built with the ocr feature; otherwise extract_page_auto falls back to the native text layer (never an error). The auto entry point is exposed in Python, Go, C#, Swift, WASM, and the C ABI. In Rust it is the library-level AutoExtractor API rather than a one-line PdfDocument method — see below.

Pythonextract_page_auto(page, options_json=None) -> str (JSON):

import json
from pdf_oxide import PdfDocument

doc = PdfDocument("mixed-scan.pdf")

# Defaults (balanced preset)
page = json.loads(doc.extract_page_auto(0))
print(page["kind"], page["confidence"], page["ocr_used"])
for region in page["regions"]:
    print(region["kind"], region["bbox"], region["reason"])

# With options
opts = json.dumps({"mode": "auto", "reconstruct_image_tables": True,
                   "ocr_languages": ["english"]})
page = json.loads(doc.extract_page_auto(0, opts))

GoExtractPageAuto(pageIndex, opts ...AutoOption) (string, error) (returns JSON; configure via functional options):

package main

import (
    "encoding/json"
    "fmt"
    "log"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    doc, err := pdfoxide.Open("mixed-scan.pdf")
    if err != nil { log.Fatal(err) }
    defer doc.Close()

    raw, err := doc.ExtractPageAuto(0)
    if err != nil { log.Fatal(err) }

    var page map[string]any
    json.Unmarshal([]byte(raw), &page)
    fmt.Println(page["kind"], page["confidence"], page["ocr_used"])
}

C#ExtractPageAuto(int pageIndex, string? optionsJson = null) -> string (JSON):

using System.Text.Json;
using PdfOxide.Core;

using var doc = PdfDocument.Open("mixed-scan.pdf");

// Defaults
string json = doc.ExtractPageAuto(0);
using var page = JsonDocument.Parse(json);
Console.WriteLine(page.RootElement.GetProperty("kind"));

// With options
string opts = """{"mode":"auto","ocr_languages":["english"]}""";
string json2 = doc.ExtractPageAuto(0, opts);

SwiftextractPageAuto(_:optionsJson:) -> String (defaults to "{}"):

let json = try doc.extractPageAuto(0, optionsJson: "{}")

JavaScript (WASM)extractPageAuto(pageIndex, optionsJson?):

import { WasmPdfDocument } from "pdf-oxide-wasm";

const doc = new WasmPdfDocument(bytes);
const page = JSON.parse(doc.extractPageAuto(0));
console.log(page.kind, page.confidence, page.ocr_used);
doc.free();

Rust — the auto path is the AutoExtractor library API. Build AutoExtractOptions (presets fast(), balanced(), high_fidelity(), or the fluent builder) and call extract_page, which returns a typed PageExtraction (no JSON round-trip):

use pdf_oxide::PdfDocument;
use pdf_oxide::extractors::auto::{AutoExtractor, AutoExtractOptions, ExtractMode};

let doc = PdfDocument::open("mixed-scan.pdf")?;

// Default (balanced) preset
let page = AutoExtractor::new().extract_page(&doc, 0)?;
println!("{:?} conf={} ocr={}", page.kind, page.confidence, page.ocr_used);

// Custom options via the builder
let opts = AutoExtractOptions::builder()
    .mode(ExtractMode::Auto)
    .reconstruct_image_tables(true)
    .ocr_languages(["english"])
    .build();
let page = AutoExtractor::with(opts).extract_page(&doc, 0)?;
for region in &page.regions {
    println!("{:?} {:?} {:?}", region.kind, region.bbox, region.reason);
}

C++extract_page_auto(page, options_json = "") returns the JSON envelope:

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("mixed-scan.pdf");
auto json = doc.extract_page_auto(0);                                    // defaults
auto json2 = doc.extract_page_auto(0, R"({"mode":"auto","ocr_languages":["english"]})");

DartextractPageAuto(page, [optionsJson]) returns the JSON envelope:

import 'dart:convert';
import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('mixed-scan.pdf');
final page = jsonDecode(doc.extractPageAuto(0));
print('${page["kind"]} ${page["confidence"]} ${page["ocr_used"]}');
doc.close();

Rpdf_extract_page_auto(doc, page, options_json = NULL) returns JSON:

library(jsonlite)

doc  <- pdf_open("mixed-scan.pdf")
page <- fromJSON(pdf_extract_page_auto(doc, 0))
cat(page$kind, page$confidence, page$ocr_used, "\n")

Juliaextract_page_auto(doc, page, options = "{}") returns JSON:

using PdfOxide, JSON

doc  = open_document("mixed-scan.pdf")
page = JSON.parse(extract_page_auto(doc, 0))
println(page["kind"], " ", page["confidence"], " ", page["ocr_used"])

ZigextractPageAuto(allocator, page, options_json) returns JSON bytes:

const json = try doc.extractPageAuto(a, 0, null);  // free json

Objective-CextractPageAuto:optionsJson:error: returns the JSON envelope:

NSString *json = [doc extractPageAuto:0 optionsJson:@"{}" error:&err];

Elixirextract_page_auto(doc, page, options_json \\ "") returns JSON:

{:ok, json} = PdfOxide.extract_page_auto(doc, 0)
page = Jason.decode!(json)
IO.inspect({page["kind"], page["confidence"], page["ocr_used"]})

Java — the auto path is the AutoExtractor API (extractPage → typed result; extractTextForPage for plain text):

import fyi.oxide.pdf.PdfDocument;
import fyi.oxide.pdf.AutoExtractor;

try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("mixed-scan.pdf"))) {
    AutoExtractor ax = AutoExtractor.of(doc);             // or .fast/.balanced/.highFidelity
    String text = ax.extractTextForPage(0);               // graceful native/OCR routing
    System.out.println(text);
}

Kotlin

import fyi.oxide.pdf.PdfDocument
import fyi.oxide.pdf.AutoExtractor

PdfDocument.open(java.nio.file.Path.of("mixed-scan.pdf")).use { doc ->
    val ax = AutoExtractor.of(doc)
    println(ax.extractTextForPage(0))
}

Scala

import fyi.oxide.pdf.{PdfDocument, AutoExtractor}
import scala.util.Using

Using.resource(PdfDocument.open("mixed-scan.pdf")) { doc =>
  val ax = AutoExtractor.of(doc)
  println(ax.extractTextForPage(0))
}

PHP — the rich JSON envelope is reachable via AutoExtractor::extractPageJson:

use PdfOxide\PdfDocument;
use PdfOxide\AutoExtractor;

$doc = PdfDocument::open('mixed-scan.pdf');
$ax  = AutoExtractor::balanced($doc);
$page = json_decode($ax->extractPageJson(0), true);
echo $page['kind'], ' ', $page['confidence'], ' ', $page['ocr_used'];

Rubyauto_extractor.extract_page(page) returns the parsed envelope merged into a Hash:

require 'pdf_oxide'

PdfOxide::PdfDocument.open('mixed-scan.pdf') do |doc|
  result = doc.auto_extractor.extract_page(0)
  cls = result[:classification]            # full PageExtraction JSON as a Hash
  puts [cls['kind'], cls['confidence'], cls['ocr_used']].join(' ')
end

How do I get structured typed regions as JSON?

For a whole-page structured view — headings, body blocks, headers/footers, page numbers, and column order — use the structured extraction entry point. It returns a StructuredPage: page_index, page_width, page_height, and a regions[] array where each region carries a kind (semantic role), text, bbox, spans, and a column_index (for multi-column reading order). Region kinds include body blocks, structural headings (H1H6), marginal labels, running headers/footers, page numbers, and artifacts.

Most bindings return this as a JSON string (the C ABI serializes it once and bindings deserialize into native types); Rust returns the typed StructuredPage directly.

C ABI signature:

char *pdf_document_extract_structured_to_json(
    PdfDocument *handle,
    int32_t page_index,
    int32_t *error_code);

Pythonextract_structured(page) -> str (JSON; deserialize with json.loads):

import json
from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
page = json.loads(doc.extract_structured(0))

print(page["page_width"], page["page_height"])
for region in page["regions"]:
    print(region["kind"], region["column_index"], region["text"][:60])

GoExtractStructured(page) (string, error):

raw, err := doc.ExtractStructured(0)
if err != nil { log.Fatal(err) }

var page map[string]any
json.Unmarshal([]byte(raw), &page)
for _, r := range page["regions"].([]any) {
    region := r.(map[string]any)
    fmt.Println(region["kind"], region["text"])
}

C#ExtractStructured(int page) -> string:

using System.Text.Json;

string json = doc.ExtractStructured(0);
using var page = JsonDocument.Parse(json);
foreach (var region in page.RootElement.GetProperty("regions").EnumerateArray())
{
    Console.WriteLine(region.GetProperty("kind"));
}

SwiftextractStructuredJson(_:) -> String:

let json = try doc.extractStructuredJson(0)

JavaScript (WASM)extractStructured(pageIndex) (returns a JSON string with camelCase keys):

const page = JSON.parse(doc.extractStructured(0));
for (const region of page.regions) {
    console.log(region.kind, region.columnIndex);
}

Rustextract_structured(page_index) -> Result<StructuredPage> returns typed regions directly (no JSON round-trip). An extract_structured_with_column_mode variant lets you force ColumnMode::Two/Single for stubborn layouts:

use pdf_oxide::PdfDocument;

let doc = PdfDocument::open("report.pdf")?;
let page = doc.extract_structured(0)?;
for region in &page.regions {
    println!("{:?} col={:?}: {}", region.kind, region.column_index, region.text);
}

C++extract_structured_json(page) returns the JSON string:

auto json = doc.extract_structured_json(0);

DartextractStructuredJson(page) returns the JSON string:

import 'dart:convert';

final page = jsonDecode(doc.extractStructuredJson(0));
for (final region in page['regions']) {
    print('${region["kind"]} ${region["column_index"]}');
}

Rpdf_extract_structured_json(doc, page) returns JSON:

library(jsonlite)

page <- fromJSON(pdf_extract_structured_json(doc, 0))
print(page$page_width)

Juliaextract_structured_json(doc, page) returns JSON:

using JSON
page = JSON.parse(extract_structured_json(doc, 0))
for region in page["regions"]
    println(region["kind"], " ", region["column_index"])
end

ZigextractStructuredJson(allocator, page) returns JSON bytes:

const json = try doc.extractStructuredJson(a, 0);  // free json

Objective-CextractStructuredJson:error: returns the JSON string:

NSString *json = [doc extractStructuredJson:0 error:&err];

Elixirextract_structured_json(doc, page) returns JSON:

{:ok, json} = PdfOxide.extract_structured_json(doc, 0)
page = Jason.decode!(json)

JavaextractStructured(page) returns the JSON string:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

String json = doc.extractStructured(0);
JsonNode page = new ObjectMapper().readTree(json);
for (JsonNode region : page.get("regions")) {
    System.out.println(region.get("kind").asText());
}

Kotlin

val json = doc.extractStructured(0)   // JSON string; parse with your library of choice

Scala

val json = doc.extractStructured(0)   // JSON string

Clojure(pdf/extract-structured doc page) returns the JSON string:

(require '[clojure.data.json :as json])

(with-open [doc (pdf/open "report.pdf")]
  (let [page (json/read-str (pdf/extract-structured doc 0))]
    (doseq [region (get page "regions")]
      (println (get region "kind") (get region "column_index")))))

Rubyextract_structured(page) returns the parsed StructuredPage Hash:

PdfOxide::PdfDocument.open('report.pdf') do |doc|
  page = doc.extract_structured(0)
  page['regions'].each { |r| puts "#{r['kind']} #{r['column_index']}" }
end

PHPextractStructured($page) returns the deserialized associative array:

$doc = PdfOxide\PdfDocument::open('report.pdf');
$page = $doc->extractStructured(0);
foreach ($page['regions'] as $region) {
    echo $region['kind'], ' ', $region['column_index'], "\n";
}

Coordinate reference

PDF uses a bottom-left origin, measured in points (1 pt = 1/72 inch). A Letter-size page is (0, 0, 612, 792). To target the top 1-inch band, write:

(x, y, w, h) = (0, 792 - 72, 612, 72)
             = (0, 720,      612, 72)

If you’re coming from an image-coordinate world (top-left origin), flip y accordingly.

To get the actual MediaBox of a page before calculating:

Python

doc = PdfDocument("doc.pdf")
mb = doc.page_media_box(0)       # (llx, lly, urx, ury)

Rust

let mb = editor.get_page_media_box(0)?;   // [f32; 4]

Javapage.mediaBox() returns a BBox (x0, y0, x1, y1):

import fyi.oxide.pdf.geometry.BBox;

BBox mb = doc.page(0).mediaBox();         // (x0, y0, x1, y1) in PDF user space
double w = mb.width(), h = mb.height();   // 612 × 792 for US Letter

Kotlin

val mb = doc.page(0).mediaBox()           // BBox(x0, y0, x1, y1)

Scala

val mb = doc.page(0).mediaBox             // BBox(x0, y0, x1, y1)

C++ — via the editor: get_page_media_box(page):

auto editor = pdf_oxide::DocumentEditor::open("doc.pdf");
auto mb = editor.get_page_media_box(0);   // Bbox{x, y, width, height}

Swift

let editor = try DocumentEditor.open("doc.pdf")
let mb = try editor.getPageMediaBox(0)    // Bbox(x, y, width, height)

Dart

final editor = DocumentEditor.open('doc.pdf');
final mb = editor.getPageMediaBox(0);     // Bbox(x, y, width, height)

R

editor <- pdf_editor_open("doc.pdf")
mb <- pdf_editor_get_page_media_box(editor, 0)   # list(x=, y=, width=, height=)

Julia

editor = open_editor("doc.pdf")
mb = get_page_media_box(editor, 0)        # Bbox

Zig

var editor = try pdf_oxide.DocumentEditor.openEditor("doc.pdf");
const mb = try editor.getPageMediaBox(0);  // Bbox{ x, y, width, height }

Objective-C

POXDocumentEditor *editor = [POXDocumentEditor openEditor:@"doc.pdf" error:&err];
POXBbox mb = [editor pageMediaBox:0 error:&err];   // {x, y, width, height}

Elixir

{:ok, editor} = PdfOxide.open_editor("doc.pdf")
{:ok, mb} = PdfOxide.get_page_media_box(editor, 0)   # %Bbox{}

Go / C# — in-rect helpers

While Go and C# don’t yet expose the fluent within() chain, the underlying lower-level methods are the same:

Method Go C#
Text in rect doc.ExtractTextInRect(page, x, y, w, h) doc.ExtractTextInRect(page, x, y, w, h)
Words in rect doc.ExtractWordsInRect(page, x, y, w, h) (not yet wrapped)
Images in rect doc.ExtractImagesInRect(page, x, y, w, h) (not yet wrapped)

For patterns that need multiple extraction types against the same rect in Go or C#, keep the rect in variables and call the helpers sequentially. The fluent surface will follow once the editor API stabilises.

FAQ

What’s the difference between extract_words() and extract_lines_in_rect() in a region? extract_words() returns one record per word; extract_lines_in_rect() returns one record per line (text, bbox, and word count) for the lines whose bbox intersects the rectangle. Use lines when you want whole rows in reading order — address blocks, statement rows, multi-line totals — without re-grouping words yourself.

Does extract_page_auto always run OCR? No. It routes per region. With the default "auto" mode it only escalates to OCR where the native text layer is missing or suspect, and OCR only actually runs when the library is built with the ocr feature. Without that feature it falls back to the native text layer and never raises an opaque OCR error.

Which bindings expose the lines-in-rect and tables-in-rect variants? Rust, the C ABI, and Swift expose extract_lines_in_rect / extract_tables_in_rect directly. Python reaches the same results through the fluent region (within(...).extract_text_lines() and within(...).extract_tables()). Go and C# don’t yet wrap the in-rect lines/tables entry points — extract for the full page and filter on the returned bounding boxes.

How fast is scoped extraction? Scoping doesn’t add measurable overhead over full-page extraction — PDF Oxide extracts at a 0.8ms mean (100% pass rate on the benchmark corpus), and an in-rect call filters that same result by bounding box.