@jackgreen2018/pdf-engine 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackgreen2018/pdf-engine",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/pkg/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pdf_engine",
3
3
  "type": "module",
4
- "version": "1.0.7",
4
+ "version": "1.0.8",
5
5
  "files": [
6
6
  "pdf_engine_bg.wasm",
7
7
  "pdf_engine.js",
Binary file
package/src/lib.rs CHANGED
@@ -15,35 +15,67 @@ pub fn get_page_count(buffer: &[u8]) -> Result<u32, JsValue> {
15
15
  Ok(doc.get_pages().len().max(1) as u32)
16
16
  }
17
17
 
18
+ /// Decode PDF hex strings (<HHHH...>) in content stream operators.
19
+ fn decode_hex_strings(content: &str) -> String {
20
+ let mut result = String::new();
21
+ let bytes = content.as_bytes();
22
+ let mut i = 0;
23
+ while i < bytes.len() {
24
+ if bytes[i] == b'<' {
25
+ let mut j = i + 1;
26
+ while j < bytes.len() && bytes[j] != b'>' {
27
+ j += 1;
28
+ }
29
+ if j > i + 1 && j < bytes.len() {
30
+ let hex = &content[i+1..j];
31
+ if hex.len() % 2 == 0 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
32
+ let mut buf = Vec::with_capacity(hex.len() / 2);
33
+ for k in (0..hex.len()).step_by(2) {
34
+ if let Ok(byte) = u8::from_str_radix(&hex[k..k+2], 16) {
35
+ buf.push(byte);
36
+ }
37
+ }
38
+ if let Ok(s) = String::from_utf8(buf) {
39
+ result.push_str(&s);
40
+ }
41
+ }
42
+ }
43
+ i = j + 1;
44
+ } else {
45
+ i += 1;
46
+ }
47
+ }
48
+ result
49
+ }
50
+
51
+ /// Extract text by iterating ALL objects and processing content streams.
52
+ fn collect_page_text(doc: &Document) -> String {
53
+ let mut text = String::new();
54
+ for (_id, obj) in doc.objects.iter() {
55
+ let Object::Stream(ref stream) = obj else { continue };
56
+ let mut s = stream.clone();
57
+ let _ = s.decompress();
58
+ let content = String::from_utf8_lossy(&s.content);
59
+ // ponytail: only process streams that look like page content (have BT/ET text blocks)
60
+ if content.contains("BT") && content.contains("ET") {
61
+ text.push_str(&decode_hex_strings(&content));
62
+ }
63
+ }
64
+ text
65
+ }
66
+
18
67
  #[wasm_bindgen]
19
68
  pub fn extract_text(buffer: &[u8]) -> Result<String, JsValue> {
20
69
  let doc = Document::load_mem(buffer)
21
70
  .map_err(|e| JsValue::from_str(&format!("PDF error: {}", e)))?;
22
71
 
23
- let mut text = String::new();
24
-
25
- for obj in doc.objects.values() {
26
- if let Object::Dictionary(ref dict) = *obj {
27
- let is_page = match dict.get(b"/Type") {
28
- Ok(Object::String(s, _)) => s == b"Page",
29
- _ => false,
30
- };
31
-
32
- if is_page {
33
- match dict.get(b"/Contents") {
34
- Ok(Object::Stream(stream)) => {
35
- text.push_str(&String::from_utf8_lossy(&stream.content));
36
- },
37
- _ => {}
38
- }
39
- }
40
- }
41
- }
72
+ let text = collect_page_text(&doc);
42
73
 
43
74
  if text.is_empty() {
44
- text = "No text extracted.".to_string();
75
+ Ok("No text extracted.".to_string())
76
+ } else {
77
+ Ok(text)
45
78
  }
46
- Ok(text)
47
79
  }
48
80
 
49
81
  /// Remap all ObjectId references in `obj` using `id_map`.
@@ -109,11 +141,9 @@ fn walk_refs(obj: &Object, doc: &Document, visited: &mut BTreeMap<ObjectId, Obje
109
141
 
110
142
  /// Copy page at `page_obj_id` from `src` into `dst`, updating all internal references.
111
143
  fn copy_page_into(src: &Document, dst: &mut Document, page_obj_id: ObjectId) -> ObjectId {
112
- // Collect all reachable objects from this page
113
144
  let mut to_copy: BTreeMap<ObjectId, Object> = BTreeMap::new();
114
145
  collect_reachable(page_obj_id, src, &mut to_copy);
115
146
 
116
- // Assign new IDs in dst's address space
117
147
  let mut id_map: BTreeMap<ObjectId, ObjectId> = BTreeMap::new();
118
148
  let mut next_id = dst.max_id + 1;
119
149
 
@@ -156,12 +186,10 @@ pub fn merge(pdf_buffers: Array) -> Result<Uint8Array, JsValue> {
156
186
  let other = Document::load_mem(&buf.to_vec())
157
187
  .map_err(|e| JsValue::from_str(&format!("PDF load error: {}", e)))?;
158
188
 
159
- // Pre-compute page count before any mutable borrow
160
189
  let page_count = doc.get_pages().len() as i64;
161
190
 
162
191
  for &page_obj_id in other.get_pages().values() {
163
192
  let new_page_id = copy_page_into(&other, &mut doc, page_obj_id);
164
- // Append new page to the Pages catalog Kids array
165
193
  if let Ok(root_ref) = doc.trailer.get(b"Root").and_then(|r| r.as_reference()) {
166
194
  if let Some(Object::Dictionary(ref mut root_dict)) = doc.objects.get_mut(&root_ref) {
167
195
  if let Ok(pages_ref) = root_dict.get(b"Pages").and_then(|p| p.as_reference()) {
@@ -192,20 +220,16 @@ pub fn split(buffer: &[u8], pages: Array) -> Result<Array, JsValue> {
192
220
  .map_err(|e| JsValue::from_str(&format!("PDF error: {}", e)))?;
193
221
  let page_len = pages.length();
194
222
 
195
- // get_pages returns BTreeMap<u32, ObjectId>
196
223
  let page_map: BTreeMap<u32, ObjectId> = doc.get_pages();
197
-
198
224
  let results = Array::new();
199
225
 
200
226
  for i in 0..page_len {
201
227
  let page_num = pages.get(i).as_f64().unwrap_or(0.0) as u32;
202
228
  let Some(&page_obj_id) = page_map.get(&page_num) else { continue };
203
229
 
204
- // Build a new document containing just this page
205
230
  let mut new_doc = Document::with_version("1.5");
206
231
  let _ = copy_page_into(&doc, &mut new_doc, page_obj_id);
207
232
 
208
- // Update the Pages catalog (first Pages dictionary in new_doc)
209
233
  if let Some((_, &pages_obj_id)) = new_doc.get_pages().iter().next() {
210
234
  let new_count = new_doc.get_pages().len() as i64;
211
235
  if let Some(Object::Dictionary(ref mut pages_dict)) = new_doc.objects.get_mut(&pages_obj_id) {