Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,10 @@ r/*.tar.gz
/elixir/deps/
/elixir/priv/
/elixir/mix.lock

# Viewer build artifacts (never commit generated output)
viewer/dotnet/**/bin/
viewer/dotnet/**/obj/
viewer/dist-browser/
viewer/**/node_modules/
viewer/**/.gradle/
10 changes: 10 additions & 0 deletions cpp/include/pdf_oxide/pdf_oxide.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ struct Element {
std::string type;
std::string text;
Bbox rect;
/// ISO 32000-1 §9.10.2 mapping-provenance label ("to_unicode"/"encoding"/
/// "predefined_cmap"/"embedded_cmap"/"actual_text"/"fallback"), or empty
/// when unknown. "fallback" means the text is a fabricated glyph-index echo,
/// not read from the file.
std::string provenance;
};

/// A single interactive AcroForm field (PHASE-8 forms).
Expand Down Expand Up @@ -899,6 +904,11 @@ class Document {
code = 0;
e.text = detail::take_string(pdf_oxide_element_get_text(list, i, &code),
code, "Document::page_get_elements");
code = 0;
if (char *p = pdf_oxide_element_get_provenance(list, i, &code)) {
e.provenance = detail::take_string(p, code,
"Document::page_get_elements");
}
Bbox b{0, 0, 0, 0};
pdf_oxide_element_get_rect(list, i, &b.x, &b.y, &b.width, &b.height,
&code);
Expand Down
12 changes: 12 additions & 0 deletions csharp/PdfOxide/Internal/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7096,6 +7096,18 @@ public static partial IntPtr pdf_oxide_element_get_text(
int index,
out int errorCode);

/// <summary>
/// Gets the §9.10.2 mapping-provenance label of an element
/// ("to_unicode"/"encoding"/"predefined_cmap"/"embedded_cmap"/
/// "actual_text"/"fallback"), or null when unknown.
/// </summary>
[LibraryImport(LibName, EntryPoint = "pdf_oxide_element_get_provenance", StringMarshalling = StringMarshalling.Utf8)]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
public static partial IntPtr pdf_oxide_element_get_provenance(
NativeHandle elements,
int index,
out int errorCode);

/// <summary>
/// Gets the bounding rectangle of an element.
/// </summary>
Expand Down
16 changes: 14 additions & 2 deletions dart/lib/pdf_oxide.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,8 @@ class _Native {
.lookupFunction<_ListStrC, _ListStrD>('pdf_oxide_element_get_type'),
elementGetText = lib
.lookupFunction<_ListStrC, _ListStrD>('pdf_oxide_element_get_text'),
elementGetProvenance = lib
.lookupFunction<_ListStrC, _ListStrD>('pdf_oxide_element_get_provenance'),
elementGetRect = lib.lookupFunction<_ElemRectC, _ElemRectD>(
'pdf_oxide_element_get_rect'),
elementsFree = lib
Expand Down Expand Up @@ -1680,6 +1682,7 @@ class _Native {
final _ElemCountD elementCount;
final _ListStrD elementGetType;
final _ListStrD elementGetText;
final _ListStrD elementGetProvenance;
final _ElemRectD elementGetRect;
final _ListFreeD elementsFree;
final _ElemJsonD elementsToJson;
Expand Down Expand Up @@ -7311,7 +7314,7 @@ class OcrEngine implements Finalizable {

/// A single layout element read from an [ElementList].
class Element {
const Element(this.type, this.text, this.rect);
const Element(this.type, this.text, this.rect, [this.provenance = '']);

/// The element type label (e.g. `Text`, `Image`, `Table`).
final String type;
Expand All @@ -7322,6 +7325,12 @@ class Element {
/// The element's bounding box in page user-space points.
final Bbox rect;

/// ISO 32000-1 §9.10.2 mapping-provenance label (`to_unicode`/`encoding`/
/// `predefined_cmap`/`embedded_cmap`/`actual_text`/`fallback`), or `''` when
/// unknown. `fallback` means the text is a fabricated glyph-index echo, not
/// read from the file.
final String provenance;

@override
String toString() => 'Element($type, $rect)';
}
Expand Down Expand Up @@ -7372,6 +7381,8 @@ class ElementList implements Finalizable {
_check();
final type = _str(_n.elementGetType, index, 'elementGetType');
final text = _str(_n.elementGetText, index, 'elementGetText');
final provenance =
_str(_n.elementGetProvenance, index, 'elementGetProvenance');
final x = calloc<Float>();
final y = calloc<Float>();
final w = calloc<Float>();
Expand All @@ -7380,7 +7391,8 @@ class ElementList implements Finalizable {
try {
_n.elementGetRect(_handle, index, x, y, w, h, code);
if (code.value != 0) throw PdfOxideError(code.value, 'elementGetRect');
return Element(type, text, Bbox(x.value, y.value, w.value, h.value));
return Element(
type, text, Bbox(x.value, y.value, w.value, h.value), provenance);
} finally {
calloc.free(x);
calloc.free(y);
Expand Down
1 change: 1 addition & 0 deletions go/pdf_oxide.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ extern void* pdf_page_get_elements(void* handle, int32_t page_index, int* error_
extern int32_t pdf_oxide_element_count(const void* elements);
extern char* pdf_oxide_element_get_type(const void* elements, int32_t index, int* error_code);
extern char* pdf_oxide_element_get_text(const void* elements, int32_t index, int* error_code);
extern char* pdf_oxide_element_get_provenance(const void* elements, int32_t index, int* error_code);
extern void pdf_oxide_element_get_rect(const void* elements, int32_t index, float* x, float* y, float* width, float* height, int* error_code);
extern void pdf_oxide_elements_free(void* handle);

Expand Down
5 changes: 5 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ type Element struct {
Y float32 `json:"y"`
Width float32 `json:"width"`
Height float32 `json:"height"`
// Provenance is the ISO 32000-1 §9.10.2 mapping tier the span's font
// offered ("to_unicode"/"encoding"/"predefined_cmap"/"embedded_cmap"/
// "actual_text"/"fallback"), or "" when unknown. "fallback" means the
// text is a fabricated glyph-index echo, not read from the file.
Provenance string `json:"provenance,omitempty"`
}

// ─── DocumentBuilder write-side value types ──────────────────────────────
Expand Down
13 changes: 13 additions & 0 deletions include/pdf_oxide_c/pdf_oxide.h
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,19 @@ char *pdf_oxide_element_get_text(const FfiElementList *elements,
int32_t *error_code);
#endif

#if !defined(PDF_OXIDE_TARGET_WASM32)
/**
* The span's Unicode-mapping provenance label (ISO 32000-1 §9.10.2 tier):
* `"to_unicode"`, `"encoding"`, `"predefined_cmap"`, `"embedded_cmap"`,
* `"actual_text"`, or `"fallback"` (the text is a fabricated glyph-index echo,
* not read from the file). Returns null when the font could not be resolved.
* Free the returned string with `pdf_oxide_free_string`.
*/
char *pdf_oxide_element_get_provenance(const FfiElementList *elements,
int32_t index,
int32_t *error_code);
#endif

#if !defined(PDF_OXIDE_TARGET_WASM32)
void pdf_oxide_element_get_rect(const FfiElementList *elements,
int32_t index,
Expand Down
5 changes: 5 additions & 0 deletions objc/include/POXPdfOxide.h
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,11 @@ typedef struct {
- (nullable NSString*)typeAtIndex:(int32_t)index error:(NSError**)error;
/// The element text at `index` (nil on error).
- (nullable NSString*)textAtIndex:(int32_t)index error:(NSError**)error;
/// The element ISO 32000-1 §9.10.2 mapping-provenance label at `index`
/// (@"to_unicode"/@"encoding"/@"predefined_cmap"/@"embedded_cmap"/
/// @"actual_text"/@"fallback"), or nil when unknown. @"fallback" means the text
/// is a fabricated glyph-index echo, not read from the file.
- (nullable NSString*)provenanceAtIndex:(int32_t)index error:(NSError**)error;
/// The element bounding box at `index`.
- (POXBbox)rectAtIndex:(int32_t)index error:(NSError**)error;
/// Serialize the whole list to JSON (nil on error).
Expand Down
6 changes: 6 additions & 0 deletions objc/src/POXPdfOxide.m
Original file line number Diff line number Diff line change
Expand Up @@ -4498,6 +4498,12 @@ - (NSString*)textAtIndex:(int32_t)index error:(NSError**)error {
@"elementText", error);
}

- (NSString*)provenanceAtIndex:(int32_t)index error:(NSError**)error {
int32_t code = 0;
return POXTakeString(pdf_oxide_element_get_provenance(_handle, index, &code), code,
@"elementProvenance", error);
}

- (POXBbox)rectAtIndex:(int32_t)index error:(NSError**)error {
int32_t code = 0;
float x = 0, y = 0, w = 0, h = 0;
Expand Down
4 changes: 4 additions & 0 deletions php/include/pdf_oxide.h
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,10 @@ char *pdf_oxide_element_get_text(const FfiElementList *elements,
int32_t index,
int32_t *error_code);

char *pdf_oxide_element_get_provenance(const FfiElementList *elements,
int32_t index,
int32_t *error_code);

void pdf_oxide_element_get_rect(const FfiElementList *elements,
int32_t index,
float *x,
Expand Down
84 changes: 84 additions & 0 deletions src/bin/corpus_sig.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! Native Rust regression-sweep signature tool.
//!
//! Prints one line per PDF in a corpus directory:
//!
//! ```text
//! <relpath>\t<text_hash>\t<nchars>\t<max_word_len>\t<page_rotations>
//! ```
//!
//! Build once per version and diff the outputs to find extraction regressions:
//!
//! ```text
//! cargo build --release --bin corpus_sig --jobs 3
//! ./target/release/corpus_sig <corpus_dir> > head.txt
//! # (in a v0.3.71 worktree) ./target/release/corpus_sig <corpus_dir> > base.txt
//! diff base.txt head.txt
//! ```
//!
//! Single process, release speed — no Python, no per-doc subprocess.

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};

use pdf_oxide::document::PdfDocument;

fn collect_pdfs(dir: &Path, out: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
let mut entries: Vec<_> = entries.flatten().map(|e| e.path()).collect();
entries.sort();
for p in entries {
if p.is_dir() {
collect_pdfs(&p, out);
} else if p.extension().and_then(|e| e.to_str()) == Some("pdf") {
out.push(p);
}
}
}
}

fn main() {
let root = std::env::args()
.nth(1)
.expect("usage: corpus_sig <corpus_dir>");
let root = PathBuf::from(root);
let mut pdfs = Vec::new();
collect_pdfs(&root, &mut pdfs);
eprintln!("corpus: {} pdfs", pdfs.len());

for pdf in &pdfs {
let rel = pdf.strip_prefix(&root).unwrap_or(pdf).display();
let doc = match PdfDocument::open(pdf) {
Ok(d) => d,
Err(_) => {
println!("{rel}\tOPEN_ERR\t0\t0\t[]");
continue;
},
};
let n = doc.page_count().unwrap_or(0);
let mut all_text = String::new();
let mut rots: Vec<i32> = Vec::with_capacity(n);
let mut max_word = 0usize;
for i in 0..n {
rots.push(doc.get_page_rotation(i).unwrap_or(0));
if let Ok(t) = doc.extract_text(i) {
all_text.push_str(&t);
all_text.push('\n');
}
if let Ok(words) = doc.extract_words(i) {
for w in &words {
max_word = max_word.max(w.text.chars().count());
}
}
}
let mut h = DefaultHasher::new();
all_text.hash(&mut h);
println!(
"{rel}\t{:016x}\t{}\t{}\t{:?}",
h.finish(),
all_text.chars().count(),
max_word,
rots
);
}
}
2 changes: 2 additions & 0 deletions src/converters/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ mod tests {
// TextSpan represents complete strings from Tj/TJ operators, not individual chars
let spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "First".to_string(),
Expand Down Expand Up @@ -807,6 +808,7 @@ mod tests {
rtl_draw_logical: false,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "Second".to_string(),
Expand Down
Loading
Loading