@gridstorm/pdf-plugin-text 0.1.2
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/README.md +32 -0
- package/dist/index.cjs +407 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +403 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @gridstorm/pdf-plugin-text
|
|
2
|
+
|
|
3
|
+
PDF text extraction and full-text search.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @gridstorm/pdf-plugin-text
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { TextPlugin } from '@gridstorm/pdf-plugin-text';
|
|
15
|
+
|
|
16
|
+
const pdf = createPDFEngine({ plugins: [TextPlugin()] });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Full text extraction**
|
|
22
|
+
- **Search with highlighting**
|
|
23
|
+
- **Page and document-level search**
|
|
24
|
+
- **Regex support**
|
|
25
|
+
|
|
26
|
+
## Documentation
|
|
27
|
+
|
|
28
|
+
[Full Documentation](https://grid-data-analytics-explorer.vercel.app/) | [GitHub](https://github.com/007krcs/grid-data)
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/text-extractor.ts
|
|
4
|
+
var DEFAULT_WORD_SPACING = 3;
|
|
5
|
+
var DEFAULT_LINE_SPACING = 5;
|
|
6
|
+
var TextExtractor = class {
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.wordSpacingThreshold = config.wordSpacingThreshold ?? DEFAULT_WORD_SPACING;
|
|
9
|
+
this.lineSpacingThreshold = config.lineSpacingThreshold ?? DEFAULT_LINE_SPACING;
|
|
10
|
+
}
|
|
11
|
+
/** Extract text content from raw text items.
|
|
12
|
+
* In Phase 2 this will accept pdf.js TextItem[]; for now it works with PdfCharInfo[]. */
|
|
13
|
+
extract(chars, _page) {
|
|
14
|
+
if (chars.length === 0) {
|
|
15
|
+
return { chars, words: [], lines: [] };
|
|
16
|
+
}
|
|
17
|
+
const words = this.segmentWords(chars);
|
|
18
|
+
const lines = this.segmentLines(words, chars);
|
|
19
|
+
return { chars, words, lines };
|
|
20
|
+
}
|
|
21
|
+
/** Build PdfCharInfo from a simple text string (for testing/placeholder). */
|
|
22
|
+
buildCharsFromString(text, startX, startY, fontSize, fontName = "Helvetica") {
|
|
23
|
+
const chars = [];
|
|
24
|
+
let x = startX;
|
|
25
|
+
const charWidth = fontSize * 0.6;
|
|
26
|
+
for (let i = 0; i < text.length; i++) {
|
|
27
|
+
const char = text[i];
|
|
28
|
+
const rect = [x, startY, x + charWidth, startY + fontSize];
|
|
29
|
+
chars.push({
|
|
30
|
+
char,
|
|
31
|
+
rect,
|
|
32
|
+
fontName,
|
|
33
|
+
fontSize,
|
|
34
|
+
transform: [fontSize, 0, 0, fontSize, x, startY]
|
|
35
|
+
});
|
|
36
|
+
x += charWidth;
|
|
37
|
+
}
|
|
38
|
+
return chars;
|
|
39
|
+
}
|
|
40
|
+
/** Segment characters into words based on spacing. */
|
|
41
|
+
segmentWords(chars) {
|
|
42
|
+
if (chars.length === 0) return [];
|
|
43
|
+
const words = [];
|
|
44
|
+
let wordStart = 0;
|
|
45
|
+
let wordChars = [chars[0]];
|
|
46
|
+
for (let i = 1; i < chars.length; i++) {
|
|
47
|
+
const prev = chars[i - 1];
|
|
48
|
+
const curr = chars[i];
|
|
49
|
+
const gap = curr.rect[0] - prev.rect[2];
|
|
50
|
+
const isSpace = curr.char === " " || prev.char === " ";
|
|
51
|
+
const isNewLine = Math.abs(curr.rect[1] - prev.rect[1]) > this.lineSpacingThreshold;
|
|
52
|
+
const isWordBreak = gap > this.wordSpacingThreshold || isSpace || isNewLine;
|
|
53
|
+
if (isWordBreak) {
|
|
54
|
+
const text = wordChars.map((c) => c.char).join("").trim();
|
|
55
|
+
if (text.length > 0) {
|
|
56
|
+
words.push({
|
|
57
|
+
text,
|
|
58
|
+
rect: this.boundingRect(wordChars),
|
|
59
|
+
charIndices: [wordStart, wordStart + wordChars.length - 1]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (curr.char !== " ") {
|
|
63
|
+
wordStart = i;
|
|
64
|
+
wordChars = [curr];
|
|
65
|
+
} else {
|
|
66
|
+
wordStart = i + 1;
|
|
67
|
+
wordChars = [];
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
wordChars.push(curr);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (wordChars.length > 0) {
|
|
74
|
+
const text = wordChars.map((c) => c.char).join("").trim();
|
|
75
|
+
if (text.length > 0) {
|
|
76
|
+
words.push({
|
|
77
|
+
text,
|
|
78
|
+
rect: this.boundingRect(wordChars),
|
|
79
|
+
charIndices: [wordStart, wordStart + wordChars.length - 1]
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return words;
|
|
84
|
+
}
|
|
85
|
+
/** Segment words into lines based on vertical position. */
|
|
86
|
+
segmentLines(words, _chars) {
|
|
87
|
+
if (words.length === 0) return [];
|
|
88
|
+
const lines = [];
|
|
89
|
+
let lineStart = 0;
|
|
90
|
+
let lineWords = [words[0]];
|
|
91
|
+
let lineY = words[0].rect[1];
|
|
92
|
+
for (let i = 1; i < words.length; i++) {
|
|
93
|
+
const word = words[i];
|
|
94
|
+
const yDiff = Math.abs(word.rect[1] - lineY);
|
|
95
|
+
if (yDiff > this.lineSpacingThreshold) {
|
|
96
|
+
lines.push(this.createLine(lineWords, lineStart));
|
|
97
|
+
lineStart = i;
|
|
98
|
+
lineWords = [word];
|
|
99
|
+
lineY = word.rect[1];
|
|
100
|
+
} else {
|
|
101
|
+
lineWords.push(word);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (lineWords.length > 0) {
|
|
105
|
+
lines.push(this.createLine(lineWords, lineStart));
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
createLine(words, startIndex) {
|
|
110
|
+
const text = words.map((w) => w.text).join(" ");
|
|
111
|
+
const rects = words.map((w) => w.rect);
|
|
112
|
+
const rect = [
|
|
113
|
+
Math.min(...rects.map((r) => r[0])),
|
|
114
|
+
Math.min(...rects.map((r) => r[1])),
|
|
115
|
+
Math.max(...rects.map((r) => r[2])),
|
|
116
|
+
Math.max(...rects.map((r) => r[3]))
|
|
117
|
+
];
|
|
118
|
+
return {
|
|
119
|
+
text,
|
|
120
|
+
rect,
|
|
121
|
+
wordIndices: [startIndex, startIndex + words.length - 1]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Compute bounding rect for a set of characters. */
|
|
125
|
+
boundingRect(chars) {
|
|
126
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
127
|
+
for (const c of chars) {
|
|
128
|
+
if (c.rect[0] < minX) minX = c.rect[0];
|
|
129
|
+
if (c.rect[1] < minY) minY = c.rect[1];
|
|
130
|
+
if (c.rect[2] > maxX) maxX = c.rect[2];
|
|
131
|
+
if (c.rect[3] > maxY) maxY = c.rect[3];
|
|
132
|
+
}
|
|
133
|
+
return [minX, minY, maxX, maxY];
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/search-engine.ts
|
|
138
|
+
var SearchEngine = class {
|
|
139
|
+
constructor() {
|
|
140
|
+
this.textContents = /* @__PURE__ */ new Map();
|
|
141
|
+
this.lastResult = {
|
|
142
|
+
matches: [],
|
|
143
|
+
totalCount: 0,
|
|
144
|
+
activeIndex: -1
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Set the text content for a page. */
|
|
148
|
+
setPageTextContent(pageIndex, textContent) {
|
|
149
|
+
this.textContents.set(pageIndex, textContent);
|
|
150
|
+
}
|
|
151
|
+
/** Clear text content for a page. */
|
|
152
|
+
clearPageTextContent(pageIndex) {
|
|
153
|
+
this.textContents.delete(pageIndex);
|
|
154
|
+
}
|
|
155
|
+
/** Clear all cached text content. */
|
|
156
|
+
clearAll() {
|
|
157
|
+
this.textContents.clear();
|
|
158
|
+
this.lastResult = { matches: [], totalCount: 0, activeIndex: -1 };
|
|
159
|
+
}
|
|
160
|
+
/** Search for a query across all loaded pages. */
|
|
161
|
+
search(query, options = {}) {
|
|
162
|
+
if (!query) {
|
|
163
|
+
this.lastResult = { matches: [], totalCount: 0, activeIndex: -1 };
|
|
164
|
+
return this.lastResult;
|
|
165
|
+
}
|
|
166
|
+
const matches = [];
|
|
167
|
+
const sortedPages = [...this.textContents.entries()].sort(
|
|
168
|
+
([a], [b]) => a - b
|
|
169
|
+
);
|
|
170
|
+
for (const [pageIndex, textContent] of sortedPages) {
|
|
171
|
+
const pageMatches = this.searchPage(
|
|
172
|
+
pageIndex,
|
|
173
|
+
textContent,
|
|
174
|
+
query,
|
|
175
|
+
options
|
|
176
|
+
);
|
|
177
|
+
matches.push(...pageMatches);
|
|
178
|
+
}
|
|
179
|
+
this.lastResult = {
|
|
180
|
+
matches,
|
|
181
|
+
totalCount: matches.length,
|
|
182
|
+
activeIndex: matches.length > 0 ? 0 : -1
|
|
183
|
+
};
|
|
184
|
+
return this.lastResult;
|
|
185
|
+
}
|
|
186
|
+
/** Navigate to the next search match. */
|
|
187
|
+
nextMatch() {
|
|
188
|
+
if (this.lastResult.totalCount === 0) return null;
|
|
189
|
+
this.lastResult.activeIndex = (this.lastResult.activeIndex + 1) % this.lastResult.totalCount;
|
|
190
|
+
return this.lastResult.matches[this.lastResult.activeIndex] ?? null;
|
|
191
|
+
}
|
|
192
|
+
/** Navigate to the previous search match. */
|
|
193
|
+
prevMatch() {
|
|
194
|
+
if (this.lastResult.totalCount === 0) return null;
|
|
195
|
+
this.lastResult.activeIndex = (this.lastResult.activeIndex - 1 + this.lastResult.totalCount) % this.lastResult.totalCount;
|
|
196
|
+
return this.lastResult.matches[this.lastResult.activeIndex] ?? null;
|
|
197
|
+
}
|
|
198
|
+
/** Get the current active match. */
|
|
199
|
+
getActiveMatch() {
|
|
200
|
+
if (this.lastResult.activeIndex < 0 || this.lastResult.activeIndex >= this.lastResult.totalCount) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return this.lastResult.matches[this.lastResult.activeIndex] ?? null;
|
|
204
|
+
}
|
|
205
|
+
/** Get the last search result. */
|
|
206
|
+
getLastResult() {
|
|
207
|
+
return this.lastResult;
|
|
208
|
+
}
|
|
209
|
+
searchPage(pageIndex, textContent, query, options) {
|
|
210
|
+
const matches = [];
|
|
211
|
+
const fullText = textContent.lines.map((l) => l.text).join("\n");
|
|
212
|
+
let pattern;
|
|
213
|
+
try {
|
|
214
|
+
if (options.regex) {
|
|
215
|
+
const flags = options.caseSensitive ? "g" : "gi";
|
|
216
|
+
pattern = new RegExp(query, flags);
|
|
217
|
+
} else {
|
|
218
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
219
|
+
const flags = options.caseSensitive ? "g" : "gi";
|
|
220
|
+
const word = options.wholeWord ? `\\b${escaped}\\b` : escaped;
|
|
221
|
+
pattern = new RegExp(word, flags);
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
return matches;
|
|
225
|
+
}
|
|
226
|
+
let match;
|
|
227
|
+
while ((match = pattern.exec(fullText)) !== null) {
|
|
228
|
+
const matchText = match[0];
|
|
229
|
+
const matchStart = match.index;
|
|
230
|
+
const matchEnd = matchStart + matchText.length;
|
|
231
|
+
const rects = this.findMatchRects(textContent, matchStart, matchEnd);
|
|
232
|
+
matches.push({
|
|
233
|
+
pageIndex,
|
|
234
|
+
charStart: matchStart,
|
|
235
|
+
charEnd: matchEnd,
|
|
236
|
+
rects,
|
|
237
|
+
text: matchText
|
|
238
|
+
});
|
|
239
|
+
if (matchText.length === 0) {
|
|
240
|
+
pattern.lastIndex++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return matches;
|
|
244
|
+
}
|
|
245
|
+
/** Find bounding rects for a text range. */
|
|
246
|
+
findMatchRects(textContent, _start, _end) {
|
|
247
|
+
const rects = [];
|
|
248
|
+
let charOffset = 0;
|
|
249
|
+
for (const line of textContent.lines) {
|
|
250
|
+
const lineEnd = charOffset + line.text.length;
|
|
251
|
+
if (lineEnd > _start && charOffset < _end) {
|
|
252
|
+
for (let wi = line.wordIndices[0]; wi <= line.wordIndices[1]; wi++) {
|
|
253
|
+
const word = textContent.words[wi];
|
|
254
|
+
if (word) {
|
|
255
|
+
rects.push(word.rect);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
charOffset = lineEnd + 1;
|
|
260
|
+
}
|
|
261
|
+
return rects.length > 0 ? rects : [[0, 0, 0, 0]];
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/text-plugin.ts
|
|
266
|
+
var INITIAL_STATE = {
|
|
267
|
+
searchQuery: "",
|
|
268
|
+
searchOptions: {},
|
|
269
|
+
searchResult: null,
|
|
270
|
+
activeMatch: null
|
|
271
|
+
};
|
|
272
|
+
function createTextPlugin() {
|
|
273
|
+
return {
|
|
274
|
+
id: "text",
|
|
275
|
+
name: "Text Extraction & Search",
|
|
276
|
+
version: "0.1.0",
|
|
277
|
+
install(context) {
|
|
278
|
+
const extractor = new TextExtractor();
|
|
279
|
+
const searchEngine = new SearchEngine();
|
|
280
|
+
context.registerState("text", { ...INITIAL_STATE });
|
|
281
|
+
const unsubExtract = context.commandBus.registerHandler(
|
|
282
|
+
"text:extract",
|
|
283
|
+
(payload) => {
|
|
284
|
+
const state = context.store.getState();
|
|
285
|
+
const page = state.pages[payload.pageIndex];
|
|
286
|
+
if (!page) return;
|
|
287
|
+
const chars = extractor.buildCharsFromString(
|
|
288
|
+
`Sample text for page ${payload.pageIndex + 1}`,
|
|
289
|
+
72,
|
|
290
|
+
// 1 inch margin
|
|
291
|
+
72,
|
|
292
|
+
12
|
|
293
|
+
);
|
|
294
|
+
const textContent = extractor.extract(chars, page);
|
|
295
|
+
context.store.setState((prev) => {
|
|
296
|
+
const pages = [...prev.pages];
|
|
297
|
+
const existing = pages[payload.pageIndex];
|
|
298
|
+
if (existing) {
|
|
299
|
+
pages[payload.pageIndex] = { ...existing, textContent };
|
|
300
|
+
}
|
|
301
|
+
return { ...prev, pages };
|
|
302
|
+
});
|
|
303
|
+
searchEngine.setPageTextContent(payload.pageIndex, textContent);
|
|
304
|
+
context.eventBus.emit("text:extracted", {
|
|
305
|
+
pageIndex: payload.pageIndex,
|
|
306
|
+
textContent
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
const unsubSearch = context.commandBus.registerHandler(
|
|
311
|
+
"text:search",
|
|
312
|
+
(payload) => {
|
|
313
|
+
const result = searchEngine.search(payload.query, payload.options);
|
|
314
|
+
context.setState("text", (prev) => ({
|
|
315
|
+
...prev,
|
|
316
|
+
searchQuery: payload.query,
|
|
317
|
+
searchOptions: payload.options ?? {},
|
|
318
|
+
searchResult: result,
|
|
319
|
+
activeMatch: searchEngine.getActiveMatch()
|
|
320
|
+
}));
|
|
321
|
+
context.eventBus.emit("search:found", {
|
|
322
|
+
query: payload.query,
|
|
323
|
+
matches: result.matches,
|
|
324
|
+
total: result.totalCount
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
const unsubSearchNext = context.commandBus.registerHandler(
|
|
329
|
+
"text:searchNext",
|
|
330
|
+
() => {
|
|
331
|
+
const match = searchEngine.nextMatch();
|
|
332
|
+
context.setState("text", (prev) => ({
|
|
333
|
+
...prev,
|
|
334
|
+
activeMatch: match,
|
|
335
|
+
searchResult: searchEngine.getLastResult()
|
|
336
|
+
}));
|
|
337
|
+
if (match) {
|
|
338
|
+
context.api.goToPage(match.pageIndex);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
const unsubSearchPrev = context.commandBus.registerHandler(
|
|
343
|
+
"text:searchPrev",
|
|
344
|
+
() => {
|
|
345
|
+
const match = searchEngine.prevMatch();
|
|
346
|
+
context.setState("text", (prev) => ({
|
|
347
|
+
...prev,
|
|
348
|
+
activeMatch: match,
|
|
349
|
+
searchResult: searchEngine.getLastResult()
|
|
350
|
+
}));
|
|
351
|
+
if (match) {
|
|
352
|
+
context.api.goToPage(match.pageIndex);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
const pluginApi = {
|
|
357
|
+
extractPageText(pageIndex) {
|
|
358
|
+
context.commandBus.dispatch("text:extract", { pageIndex });
|
|
359
|
+
},
|
|
360
|
+
extractAllText() {
|
|
361
|
+
const state = context.store.getState();
|
|
362
|
+
for (let i = 0; i < state.pages.length; i++) {
|
|
363
|
+
context.commandBus.dispatch("text:extract", { pageIndex: i });
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
search(query, options) {
|
|
367
|
+
context.commandBus.dispatch("text:search", { query, options });
|
|
368
|
+
return searchEngine.getLastResult();
|
|
369
|
+
},
|
|
370
|
+
searchNext() {
|
|
371
|
+
context.commandBus.dispatch("text:searchNext", {});
|
|
372
|
+
return searchEngine.getActiveMatch();
|
|
373
|
+
},
|
|
374
|
+
searchPrev() {
|
|
375
|
+
context.commandBus.dispatch("text:searchPrev", {});
|
|
376
|
+
return searchEngine.getActiveMatch();
|
|
377
|
+
},
|
|
378
|
+
clearSearch() {
|
|
379
|
+
searchEngine.clearAll();
|
|
380
|
+
context.setState("text", () => ({
|
|
381
|
+
...INITIAL_STATE
|
|
382
|
+
}));
|
|
383
|
+
},
|
|
384
|
+
getExtractor() {
|
|
385
|
+
return extractor;
|
|
386
|
+
},
|
|
387
|
+
getSearchEngine() {
|
|
388
|
+
return searchEngine;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
context._pluginApi = pluginApi;
|
|
392
|
+
return () => {
|
|
393
|
+
unsubExtract();
|
|
394
|
+
unsubSearch();
|
|
395
|
+
unsubSearchNext();
|
|
396
|
+
unsubSearchPrev();
|
|
397
|
+
searchEngine.clearAll();
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
exports.SearchEngine = SearchEngine;
|
|
404
|
+
exports.TextExtractor = TextExtractor;
|
|
405
|
+
exports.createTextPlugin = createTextPlugin;
|
|
406
|
+
//# sourceMappingURL=index.cjs.map
|
|
407
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/text-extractor.ts","../src/search-engine.ts","../src/text-plugin.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// ─── Text Extractor ───\n//\n// Extracts text content from PDF pages with character, word, and line bounding boxes.\n// Phase 1 implementation uses synthetic extraction; Phase 2 will integrate pdf.js getTextContent().\n\nimport type {\n PdfTextContent,\n PdfCharInfo,\n PdfWordInfo,\n PdfLineInfo,\n PdfRect,\n PdfPageState,\n} from '@gridstorm/pdf-core';\n\n/** Configuration for text extraction. */\nexport interface TextExtractorConfig {\n /** Threshold for word break detection (in PDF points). */\n wordSpacingThreshold?: number;\n /** Threshold for line break detection (in PDF points). */\n lineSpacingThreshold?: number;\n}\n\nconst DEFAULT_WORD_SPACING = 3;\nconst DEFAULT_LINE_SPACING = 5;\n\n/** Extracts structured text content from PDF text items. */\nexport class TextExtractor {\n private wordSpacingThreshold: number;\n private lineSpacingThreshold: number;\n\n constructor(config: TextExtractorConfig = {}) {\n this.wordSpacingThreshold = config.wordSpacingThreshold ?? DEFAULT_WORD_SPACING;\n this.lineSpacingThreshold = config.lineSpacingThreshold ?? DEFAULT_LINE_SPACING;\n }\n\n /** Extract text content from raw text items.\n * In Phase 2 this will accept pdf.js TextItem[]; for now it works with PdfCharInfo[]. */\n extract(chars: PdfCharInfo[], _page: PdfPageState): PdfTextContent {\n if (chars.length === 0) {\n return { chars, words: [], lines: [] };\n }\n\n const words = this.segmentWords(chars);\n const lines = this.segmentLines(words, chars);\n\n return { chars, words, lines };\n }\n\n /** Build PdfCharInfo from a simple text string (for testing/placeholder). */\n buildCharsFromString(\n text: string,\n startX: number,\n startY: number,\n fontSize: number,\n fontName = 'Helvetica',\n ): PdfCharInfo[] {\n const chars: PdfCharInfo[] = [];\n let x = startX;\n const charWidth = fontSize * 0.6; // Approximate monospace width\n\n for (let i = 0; i < text.length; i++) {\n const char = text[i]!;\n const rect: PdfRect = [x, startY, x + charWidth, startY + fontSize];\n\n chars.push({\n char,\n rect,\n fontName,\n fontSize,\n transform: [fontSize, 0, 0, fontSize, x, startY],\n });\n\n x += charWidth;\n }\n\n return chars;\n }\n\n /** Segment characters into words based on spacing. */\n private segmentWords(chars: PdfCharInfo[]): PdfWordInfo[] {\n if (chars.length === 0) return [];\n\n const words: PdfWordInfo[] = [];\n let wordStart = 0;\n let wordChars: PdfCharInfo[] = [chars[0]!];\n\n for (let i = 1; i < chars.length; i++) {\n const prev = chars[i - 1]!;\n const curr = chars[i]!;\n\n const gap = curr.rect[0] - prev.rect[2]; // x1 of current - x2 of previous\n const isSpace = curr.char === ' ' || prev.char === ' ';\n const isNewLine = Math.abs(curr.rect[1] - prev.rect[1]) > this.lineSpacingThreshold;\n const isWordBreak = gap > this.wordSpacingThreshold || isSpace || isNewLine;\n\n if (isWordBreak) {\n // Complete current word (skip if only spaces)\n const text = wordChars.map((c) => c.char).join('').trim();\n if (text.length > 0) {\n words.push({\n text,\n rect: this.boundingRect(wordChars),\n charIndices: [wordStart, wordStart + wordChars.length - 1],\n });\n }\n\n // Skip space characters\n if (curr.char !== ' ') {\n wordStart = i;\n wordChars = [curr];\n } else {\n wordStart = i + 1;\n wordChars = [];\n }\n } else {\n wordChars.push(curr);\n }\n }\n\n // Final word\n if (wordChars.length > 0) {\n const text = wordChars.map((c) => c.char).join('').trim();\n if (text.length > 0) {\n words.push({\n text,\n rect: this.boundingRect(wordChars),\n charIndices: [wordStart, wordStart + wordChars.length - 1],\n });\n }\n }\n\n return words;\n }\n\n /** Segment words into lines based on vertical position. */\n private segmentLines(words: PdfWordInfo[], _chars: PdfCharInfo[]): PdfLineInfo[] {\n if (words.length === 0) return [];\n\n const lines: PdfLineInfo[] = [];\n let lineStart = 0;\n let lineWords: PdfWordInfo[] = [words[0]!];\n let lineY = words[0]!.rect[1]; // y1 of first word\n\n for (let i = 1; i < words.length; i++) {\n const word = words[i]!;\n const yDiff = Math.abs(word.rect[1] - lineY);\n\n if (yDiff > this.lineSpacingThreshold) {\n // New line\n lines.push(this.createLine(lineWords, lineStart));\n lineStart = i;\n lineWords = [word];\n lineY = word.rect[1];\n } else {\n lineWords.push(word);\n }\n }\n\n // Final line\n if (lineWords.length > 0) {\n lines.push(this.createLine(lineWords, lineStart));\n }\n\n return lines;\n }\n\n private createLine(words: PdfWordInfo[], startIndex: number): PdfLineInfo {\n const text = words.map((w) => w.text).join(' ');\n const rects = words.map((w) => w.rect);\n const rect: PdfRect = [\n Math.min(...rects.map((r) => r[0])),\n Math.min(...rects.map((r) => r[1])),\n Math.max(...rects.map((r) => r[2])),\n Math.max(...rects.map((r) => r[3])),\n ];\n return {\n text,\n rect,\n wordIndices: [startIndex, startIndex + words.length - 1],\n };\n }\n\n /** Compute bounding rect for a set of characters. */\n private boundingRect(chars: PdfCharInfo[]): PdfRect {\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n\n for (const c of chars) {\n if (c.rect[0] < minX) minX = c.rect[0];\n if (c.rect[1] < minY) minY = c.rect[1];\n if (c.rect[2] > maxX) maxX = c.rect[2];\n if (c.rect[3] > maxY) maxY = c.rect[3];\n }\n\n return [minX, minY, maxX, maxY];\n }\n}\n","// ─── Search Engine ───\n//\n// Full-text search across PDF pages with match navigation.\n\nimport type { PdfTextContent, PdfRect } from '@gridstorm/pdf-core';\n\n/** Search options. */\nexport interface SearchOptions {\n /** Case-sensitive search. */\n caseSensitive?: boolean;\n /** Match whole words only. */\n wholeWord?: boolean;\n /** Use regex pattern. */\n regex?: boolean;\n}\n\n/** A single search match. */\nexport interface SearchMatch {\n /** Page index where the match was found. */\n pageIndex: number;\n /** Start character index within the page text. */\n charStart: number;\n /** End character index (exclusive). */\n charEnd: number;\n /** Bounding rectangles covering the match (may span multiple lines). */\n rects: PdfRect[];\n /** The matched text. */\n text: string;\n}\n\n/** Search result set. */\nexport interface SearchResult {\n /** All matches across all pages. */\n matches: SearchMatch[];\n /** Total match count. */\n totalCount: number;\n /** Currently active match index (-1 if none). */\n activeIndex: number;\n}\n\n/** Search engine for finding text in PDF pages. */\nexport class SearchEngine {\n private textContents = new Map<number, PdfTextContent>();\n private lastResult: SearchResult = {\n matches: [],\n totalCount: 0,\n activeIndex: -1,\n };\n\n /** Set the text content for a page. */\n setPageTextContent(pageIndex: number, textContent: PdfTextContent): void {\n this.textContents.set(pageIndex, textContent);\n }\n\n /** Clear text content for a page. */\n clearPageTextContent(pageIndex: number): void {\n this.textContents.delete(pageIndex);\n }\n\n /** Clear all cached text content. */\n clearAll(): void {\n this.textContents.clear();\n this.lastResult = { matches: [], totalCount: 0, activeIndex: -1 };\n }\n\n /** Search for a query across all loaded pages. */\n search(query: string, options: SearchOptions = {}): SearchResult {\n if (!query) {\n this.lastResult = { matches: [], totalCount: 0, activeIndex: -1 };\n return this.lastResult;\n }\n\n const matches: SearchMatch[] = [];\n\n // Sort pages by index for consistent ordering\n const sortedPages = [...this.textContents.entries()].sort(\n ([a], [b]) => a - b,\n );\n\n for (const [pageIndex, textContent] of sortedPages) {\n const pageMatches = this.searchPage(\n pageIndex,\n textContent,\n query,\n options,\n );\n matches.push(...pageMatches);\n }\n\n this.lastResult = {\n matches,\n totalCount: matches.length,\n activeIndex: matches.length > 0 ? 0 : -1,\n };\n\n return this.lastResult;\n }\n\n /** Navigate to the next search match. */\n nextMatch(): SearchMatch | null {\n if (this.lastResult.totalCount === 0) return null;\n\n this.lastResult.activeIndex =\n (this.lastResult.activeIndex + 1) % this.lastResult.totalCount;\n return this.lastResult.matches[this.lastResult.activeIndex] ?? null;\n }\n\n /** Navigate to the previous search match. */\n prevMatch(): SearchMatch | null {\n if (this.lastResult.totalCount === 0) return null;\n\n this.lastResult.activeIndex =\n (this.lastResult.activeIndex - 1 + this.lastResult.totalCount) %\n this.lastResult.totalCount;\n return this.lastResult.matches[this.lastResult.activeIndex] ?? null;\n }\n\n /** Get the current active match. */\n getActiveMatch(): SearchMatch | null {\n if (\n this.lastResult.activeIndex < 0 ||\n this.lastResult.activeIndex >= this.lastResult.totalCount\n ) {\n return null;\n }\n return this.lastResult.matches[this.lastResult.activeIndex] ?? null;\n }\n\n /** Get the last search result. */\n getLastResult(): SearchResult {\n return this.lastResult;\n }\n\n private searchPage(\n pageIndex: number,\n textContent: PdfTextContent,\n query: string,\n options: SearchOptions,\n ): SearchMatch[] {\n const matches: SearchMatch[] = [];\n\n // Build full page text from lines\n const fullText = textContent.lines.map((l) => l.text).join('\\n');\n\n let pattern: RegExp;\n try {\n if (options.regex) {\n const flags = options.caseSensitive ? 'g' : 'gi';\n pattern = new RegExp(query, flags);\n } else {\n const escaped = query.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const flags = options.caseSensitive ? 'g' : 'gi';\n const word = options.wholeWord ? `\\\\b${escaped}\\\\b` : escaped;\n pattern = new RegExp(word, flags);\n }\n } catch {\n return matches;\n }\n\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(fullText)) !== null) {\n const matchText = match[0];\n const matchStart = match.index;\n const matchEnd = matchStart + matchText.length;\n\n // Find covering word rects\n const rects = this.findMatchRects(textContent, matchStart, matchEnd);\n\n matches.push({\n pageIndex,\n charStart: matchStart,\n charEnd: matchEnd,\n rects,\n text: matchText,\n });\n\n // Avoid infinite loop for zero-length matches\n if (matchText.length === 0) {\n pattern.lastIndex++;\n }\n }\n\n return matches;\n }\n\n /** Find bounding rects for a text range. */\n private findMatchRects(\n textContent: PdfTextContent,\n _start: number,\n _end: number,\n ): PdfRect[] {\n // In Phase 1 we use word-level rects; Phase 2 will use char-level precision.\n // For now, return rects for all words that overlap the match range.\n const rects: PdfRect[] = [];\n\n // Simple approach: iterate lines and match by character offset\n let charOffset = 0;\n for (const line of textContent.lines) {\n const lineEnd = charOffset + line.text.length;\n\n if (lineEnd > _start && charOffset < _end) {\n // This line overlaps the match — use word-level rects within the line\n for (let wi = line.wordIndices[0]; wi <= line.wordIndices[1]; wi++) {\n const word = textContent.words[wi];\n if (word) {\n rects.push(word.rect);\n }\n }\n }\n\n charOffset = lineEnd + 1; // +1 for \\n\n }\n\n return rects.length > 0 ? rects : [[0, 0, 0, 0]];\n }\n}\n","// ─── Text Plugin ───\n//\n// GridStorm PDF plugin for text extraction and search.\n\nimport type {\n PdfPlugin,\n PdfPluginContext,\n PdfPluginDisposer,\n} from '@gridstorm/pdf-core';\nimport { TextExtractor } from './text-extractor';\nimport { SearchEngine } from './search-engine';\nimport type { SearchOptions, SearchResult, SearchMatch } from './search-engine';\n\n/** Plugin state for text operations. */\nexport interface TextPluginState {\n /** Current search query. */\n searchQuery: string;\n /** Search options. */\n searchOptions: SearchOptions;\n /** Last search result. */\n searchResult: SearchResult | null;\n /** Active search match. */\n activeMatch: SearchMatch | null;\n}\n\n/** Public API exposed by the text plugin. */\nexport interface TextPluginApi {\n /** Extract text from a specific page. */\n extractPageText(pageIndex: number): void;\n /** Extract text from all pages. */\n extractAllText(): void;\n /** Search for text across all pages. */\n search(query: string, options?: SearchOptions): SearchResult;\n /** Navigate to next search match. */\n searchNext(): SearchMatch | null;\n /** Navigate to previous search match. */\n searchPrev(): SearchMatch | null;\n /** Clear search results. */\n clearSearch(): void;\n /** Get the text extractor instance. */\n getExtractor(): TextExtractor;\n /** Get the search engine instance. */\n getSearchEngine(): SearchEngine;\n}\n\nconst INITIAL_STATE: TextPluginState = {\n searchQuery: '',\n searchOptions: {},\n searchResult: null,\n activeMatch: null,\n};\n\n/** Create the text extraction and search plugin. */\nexport function createTextPlugin(): PdfPlugin {\n return {\n id: 'text',\n name: 'Text Extraction & Search',\n version: '0.1.0',\n\n install(context: PdfPluginContext): PdfPluginDisposer {\n const extractor = new TextExtractor();\n const searchEngine = new SearchEngine();\n\n // Register plugin state\n context.registerState<TextPluginState>('text', { ...INITIAL_STATE });\n\n // ─── Command Handlers ───\n\n const unsubExtract = context.commandBus.registerHandler(\n 'text:extract',\n (payload: { pageIndex: number }) => {\n const state = context.store.getState();\n const page = state.pages[payload.pageIndex];\n if (!page) return;\n\n // In Phase 1, use placeholder chars; Phase 2 will use pdf.js\n const chars = extractor.buildCharsFromString(\n `Sample text for page ${payload.pageIndex + 1}`,\n 72, // 1 inch margin\n 72,\n 12,\n );\n\n const textContent = extractor.extract(chars, page);\n\n // Update page state with extracted text\n context.store.setState((prev) => {\n const pages = [...prev.pages];\n const existing = pages[payload.pageIndex];\n if (existing) {\n pages[payload.pageIndex] = { ...existing, textContent };\n }\n return { ...prev, pages };\n });\n\n // Feed text to search engine\n searchEngine.setPageTextContent(payload.pageIndex, textContent);\n\n context.eventBus.emit('text:extracted', {\n pageIndex: payload.pageIndex,\n textContent,\n });\n },\n );\n\n const unsubSearch = context.commandBus.registerHandler(\n 'text:search',\n (payload: { query: string; options?: SearchOptions }) => {\n const result = searchEngine.search(payload.query, payload.options);\n\n context.setState<TextPluginState>('text', (prev) => ({\n ...prev,\n searchQuery: payload.query,\n searchOptions: payload.options ?? {},\n searchResult: result,\n activeMatch: searchEngine.getActiveMatch(),\n }));\n\n context.eventBus.emit('search:found', {\n query: payload.query,\n matches: result.matches,\n total: result.totalCount,\n });\n },\n );\n\n const unsubSearchNext = context.commandBus.registerHandler(\n 'text:searchNext',\n () => {\n const match = searchEngine.nextMatch();\n context.setState<TextPluginState>('text', (prev) => ({\n ...prev,\n activeMatch: match,\n searchResult: searchEngine.getLastResult(),\n }));\n\n if (match) {\n // Navigate to the match page\n context.api.goToPage(match.pageIndex);\n }\n },\n );\n\n const unsubSearchPrev = context.commandBus.registerHandler(\n 'text:searchPrev',\n () => {\n const match = searchEngine.prevMatch();\n context.setState<TextPluginState>('text', (prev) => ({\n ...prev,\n activeMatch: match,\n searchResult: searchEngine.getLastResult(),\n }));\n\n if (match) {\n context.api.goToPage(match.pageIndex);\n }\n },\n );\n\n // ─── Plugin API ───\n\n const pluginApi: TextPluginApi = {\n extractPageText(pageIndex: number) {\n context.commandBus.dispatch('text:extract', { pageIndex });\n },\n extractAllText() {\n const state = context.store.getState();\n for (let i = 0; i < state.pages.length; i++) {\n context.commandBus.dispatch('text:extract', { pageIndex: i });\n }\n },\n search(query: string, options?: SearchOptions) {\n context.commandBus.dispatch('text:search', { query, options });\n return searchEngine.getLastResult();\n },\n searchNext() {\n context.commandBus.dispatch('text:searchNext', {});\n return searchEngine.getActiveMatch();\n },\n searchPrev() {\n context.commandBus.dispatch('text:searchPrev', {});\n return searchEngine.getActiveMatch();\n },\n clearSearch() {\n searchEngine.clearAll();\n context.setState<TextPluginState>('text', () => ({\n ...INITIAL_STATE,\n }));\n },\n getExtractor() {\n return extractor;\n },\n getSearchEngine() {\n return searchEngine;\n },\n };\n\n // Expose API via plugin context (can be retrieved via api.getPluginApi('text'))\n (context as any)._pluginApi = pluginApi;\n\n return () => {\n unsubExtract();\n unsubSearch();\n unsubSearchNext();\n unsubSearchPrev();\n searchEngine.clearAll();\n };\n },\n };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { PdfCharInfo, PdfPageState, PdfTextContent, PdfRect, PdfPlugin } from '@gridstorm/pdf-core';
|
|
2
|
+
|
|
3
|
+
/** Configuration for text extraction. */
|
|
4
|
+
interface TextExtractorConfig {
|
|
5
|
+
/** Threshold for word break detection (in PDF points). */
|
|
6
|
+
wordSpacingThreshold?: number;
|
|
7
|
+
/** Threshold for line break detection (in PDF points). */
|
|
8
|
+
lineSpacingThreshold?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Extracts structured text content from PDF text items. */
|
|
11
|
+
declare class TextExtractor {
|
|
12
|
+
private wordSpacingThreshold;
|
|
13
|
+
private lineSpacingThreshold;
|
|
14
|
+
constructor(config?: TextExtractorConfig);
|
|
15
|
+
/** Extract text content from raw text items.
|
|
16
|
+
* In Phase 2 this will accept pdf.js TextItem[]; for now it works with PdfCharInfo[]. */
|
|
17
|
+
extract(chars: PdfCharInfo[], _page: PdfPageState): PdfTextContent;
|
|
18
|
+
/** Build PdfCharInfo from a simple text string (for testing/placeholder). */
|
|
19
|
+
buildCharsFromString(text: string, startX: number, startY: number, fontSize: number, fontName?: string): PdfCharInfo[];
|
|
20
|
+
/** Segment characters into words based on spacing. */
|
|
21
|
+
private segmentWords;
|
|
22
|
+
/** Segment words into lines based on vertical position. */
|
|
23
|
+
private segmentLines;
|
|
24
|
+
private createLine;
|
|
25
|
+
/** Compute bounding rect for a set of characters. */
|
|
26
|
+
private boundingRect;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Search options. */
|
|
30
|
+
interface SearchOptions {
|
|
31
|
+
/** Case-sensitive search. */
|
|
32
|
+
caseSensitive?: boolean;
|
|
33
|
+
/** Match whole words only. */
|
|
34
|
+
wholeWord?: boolean;
|
|
35
|
+
/** Use regex pattern. */
|
|
36
|
+
regex?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** A single search match. */
|
|
39
|
+
interface SearchMatch {
|
|
40
|
+
/** Page index where the match was found. */
|
|
41
|
+
pageIndex: number;
|
|
42
|
+
/** Start character index within the page text. */
|
|
43
|
+
charStart: number;
|
|
44
|
+
/** End character index (exclusive). */
|
|
45
|
+
charEnd: number;
|
|
46
|
+
/** Bounding rectangles covering the match (may span multiple lines). */
|
|
47
|
+
rects: PdfRect[];
|
|
48
|
+
/** The matched text. */
|
|
49
|
+
text: string;
|
|
50
|
+
}
|
|
51
|
+
/** Search result set. */
|
|
52
|
+
interface SearchResult {
|
|
53
|
+
/** All matches across all pages. */
|
|
54
|
+
matches: SearchMatch[];
|
|
55
|
+
/** Total match count. */
|
|
56
|
+
totalCount: number;
|
|
57
|
+
/** Currently active match index (-1 if none). */
|
|
58
|
+
activeIndex: number;
|
|
59
|
+
}
|
|
60
|
+
/** Search engine for finding text in PDF pages. */
|
|
61
|
+
declare class SearchEngine {
|
|
62
|
+
private textContents;
|
|
63
|
+
private lastResult;
|
|
64
|
+
/** Set the text content for a page. */
|
|
65
|
+
setPageTextContent(pageIndex: number, textContent: PdfTextContent): void;
|
|
66
|
+
/** Clear text content for a page. */
|
|
67
|
+
clearPageTextContent(pageIndex: number): void;
|
|
68
|
+
/** Clear all cached text content. */
|
|
69
|
+
clearAll(): void;
|
|
70
|
+
/** Search for a query across all loaded pages. */
|
|
71
|
+
search(query: string, options?: SearchOptions): SearchResult;
|
|
72
|
+
/** Navigate to the next search match. */
|
|
73
|
+
nextMatch(): SearchMatch | null;
|
|
74
|
+
/** Navigate to the previous search match. */
|
|
75
|
+
prevMatch(): SearchMatch | null;
|
|
76
|
+
/** Get the current active match. */
|
|
77
|
+
getActiveMatch(): SearchMatch | null;
|
|
78
|
+
/** Get the last search result. */
|
|
79
|
+
getLastResult(): SearchResult;
|
|
80
|
+
private searchPage;
|
|
81
|
+
/** Find bounding rects for a text range. */
|
|
82
|
+
private findMatchRects;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Plugin state for text operations. */
|
|
86
|
+
interface TextPluginState {
|
|
87
|
+
/** Current search query. */
|
|
88
|
+
searchQuery: string;
|
|
89
|
+
/** Search options. */
|
|
90
|
+
searchOptions: SearchOptions;
|
|
91
|
+
/** Last search result. */
|
|
92
|
+
searchResult: SearchResult | null;
|
|
93
|
+
/** Active search match. */
|
|
94
|
+
activeMatch: SearchMatch | null;
|
|
95
|
+
}
|
|
96
|
+
/** Public API exposed by the text plugin. */
|
|
97
|
+
interface TextPluginApi {
|
|
98
|
+
/** Extract text from a specific page. */
|
|
99
|
+
extractPageText(pageIndex: number): void;
|
|
100
|
+
/** Extract text from all pages. */
|
|
101
|
+
extractAllText(): void;
|
|
102
|
+
/** Search for text across all pages. */
|
|
103
|
+
search(query: string, options?: SearchOptions): SearchResult;
|
|
104
|
+
/** Navigate to next search match. */
|
|
105
|
+
searchNext(): SearchMatch | null;
|
|
106
|
+
/** Navigate to previous search match. */
|
|
107
|
+
searchPrev(): SearchMatch | null;
|
|
108
|
+
/** Clear search results. */
|
|
109
|
+
clearSearch(): void;
|
|
110
|
+
/** Get the text extractor instance. */
|
|
111
|
+
getExtractor(): TextExtractor;
|
|
112
|
+
/** Get the search engine instance. */
|
|
113
|
+
getSearchEngine(): SearchEngine;
|
|
114
|
+
}
|
|
115
|
+
/** Create the text extraction and search plugin. */
|
|
116
|
+
declare function createTextPlugin(): PdfPlugin;
|
|
117
|
+
|
|
118
|
+
export { SearchEngine, type SearchMatch, type SearchOptions, type SearchResult, TextExtractor, type TextExtractorConfig, type TextPluginApi, type TextPluginState, createTextPlugin };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { PdfCharInfo, PdfPageState, PdfTextContent, PdfRect, PdfPlugin } from '@gridstorm/pdf-core';
|
|
2
|
+
|
|
3
|
+
/** Configuration for text extraction. */
|
|
4
|
+
interface TextExtractorConfig {
|
|
5
|
+
/** Threshold for word break detection (in PDF points). */
|
|
6
|
+
wordSpacingThreshold?: number;
|
|
7
|
+
/** Threshold for line break detection (in PDF points). */
|
|
8
|
+
lineSpacingThreshold?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Extracts structured text content from PDF text items. */
|
|
11
|
+
declare class TextExtractor {
|
|
12
|
+
private wordSpacingThreshold;
|
|
13
|
+
private lineSpacingThreshold;
|
|
14
|
+
constructor(config?: TextExtractorConfig);
|
|
15
|
+
/** Extract text content from raw text items.
|
|
16
|
+
* In Phase 2 this will accept pdf.js TextItem[]; for now it works with PdfCharInfo[]. */
|
|
17
|
+
extract(chars: PdfCharInfo[], _page: PdfPageState): PdfTextContent;
|
|
18
|
+
/** Build PdfCharInfo from a simple text string (for testing/placeholder). */
|
|
19
|
+
buildCharsFromString(text: string, startX: number, startY: number, fontSize: number, fontName?: string): PdfCharInfo[];
|
|
20
|
+
/** Segment characters into words based on spacing. */
|
|
21
|
+
private segmentWords;
|
|
22
|
+
/** Segment words into lines based on vertical position. */
|
|
23
|
+
private segmentLines;
|
|
24
|
+
private createLine;
|
|
25
|
+
/** Compute bounding rect for a set of characters. */
|
|
26
|
+
private boundingRect;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Search options. */
|
|
30
|
+
interface SearchOptions {
|
|
31
|
+
/** Case-sensitive search. */
|
|
32
|
+
caseSensitive?: boolean;
|
|
33
|
+
/** Match whole words only. */
|
|
34
|
+
wholeWord?: boolean;
|
|
35
|
+
/** Use regex pattern. */
|
|
36
|
+
regex?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** A single search match. */
|
|
39
|
+
interface SearchMatch {
|
|
40
|
+
/** Page index where the match was found. */
|
|
41
|
+
pageIndex: number;
|
|
42
|
+
/** Start character index within the page text. */
|
|
43
|
+
charStart: number;
|
|
44
|
+
/** End character index (exclusive). */
|
|
45
|
+
charEnd: number;
|
|
46
|
+
/** Bounding rectangles covering the match (may span multiple lines). */
|
|
47
|
+
rects: PdfRect[];
|
|
48
|
+
/** The matched text. */
|
|
49
|
+
text: string;
|
|
50
|
+
}
|
|
51
|
+
/** Search result set. */
|
|
52
|
+
interface SearchResult {
|
|
53
|
+
/** All matches across all pages. */
|
|
54
|
+
matches: SearchMatch[];
|
|
55
|
+
/** Total match count. */
|
|
56
|
+
totalCount: number;
|
|
57
|
+
/** Currently active match index (-1 if none). */
|
|
58
|
+
activeIndex: number;
|
|
59
|
+
}
|
|
60
|
+
/** Search engine for finding text in PDF pages. */
|
|
61
|
+
declare class SearchEngine {
|
|
62
|
+
private textContents;
|
|
63
|
+
private lastResult;
|
|
64
|
+
/** Set the text content for a page. */
|
|
65
|
+
setPageTextContent(pageIndex: number, textContent: PdfTextContent): void;
|
|
66
|
+
/** Clear text content for a page. */
|
|
67
|
+
clearPageTextContent(pageIndex: number): void;
|
|
68
|
+
/** Clear all cached text content. */
|
|
69
|
+
clearAll(): void;
|
|
70
|
+
/** Search for a query across all loaded pages. */
|
|
71
|
+
search(query: string, options?: SearchOptions): SearchResult;
|
|
72
|
+
/** Navigate to the next search match. */
|
|
73
|
+
nextMatch(): SearchMatch | null;
|
|
74
|
+
/** Navigate to the previous search match. */
|
|
75
|
+
prevMatch(): SearchMatch | null;
|
|
76
|
+
/** Get the current active match. */
|
|
77
|
+
getActiveMatch(): SearchMatch | null;
|
|
78
|
+
/** Get the last search result. */
|
|
79
|
+
getLastResult(): SearchResult;
|
|
80
|
+
private searchPage;
|
|
81
|
+
/** Find bounding rects for a text range. */
|
|
82
|
+
private findMatchRects;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Plugin state for text operations. */
|
|
86
|
+
interface TextPluginState {
|
|
87
|
+
/** Current search query. */
|
|
88
|
+
searchQuery: string;
|
|
89
|
+
/** Search options. */
|
|
90
|
+
searchOptions: SearchOptions;
|
|
91
|
+
/** Last search result. */
|
|
92
|
+
searchResult: SearchResult | null;
|
|
93
|
+
/** Active search match. */
|
|
94
|
+
activeMatch: SearchMatch | null;
|
|
95
|
+
}
|
|
96
|
+
/** Public API exposed by the text plugin. */
|
|
97
|
+
interface TextPluginApi {
|
|
98
|
+
/** Extract text from a specific page. */
|
|
99
|
+
extractPageText(pageIndex: number): void;
|
|
100
|
+
/** Extract text from all pages. */
|
|
101
|
+
extractAllText(): void;
|
|
102
|
+
/** Search for text across all pages. */
|
|
103
|
+
search(query: string, options?: SearchOptions): SearchResult;
|
|
104
|
+
/** Navigate to next search match. */
|
|
105
|
+
searchNext(): SearchMatch | null;
|
|
106
|
+
/** Navigate to previous search match. */
|
|
107
|
+
searchPrev(): SearchMatch | null;
|
|
108
|
+
/** Clear search results. */
|
|
109
|
+
clearSearch(): void;
|
|
110
|
+
/** Get the text extractor instance. */
|
|
111
|
+
getExtractor(): TextExtractor;
|
|
112
|
+
/** Get the search engine instance. */
|
|
113
|
+
getSearchEngine(): SearchEngine;
|
|
114
|
+
}
|
|
115
|
+
/** Create the text extraction and search plugin. */
|
|
116
|
+
declare function createTextPlugin(): PdfPlugin;
|
|
117
|
+
|
|
118
|
+
export { SearchEngine, type SearchMatch, type SearchOptions, type SearchResult, TextExtractor, type TextExtractorConfig, type TextPluginApi, type TextPluginState, createTextPlugin };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// src/text-extractor.ts
|
|
2
|
+
var DEFAULT_WORD_SPACING = 3;
|
|
3
|
+
var DEFAULT_LINE_SPACING = 5;
|
|
4
|
+
var TextExtractor = class {
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.wordSpacingThreshold = config.wordSpacingThreshold ?? DEFAULT_WORD_SPACING;
|
|
7
|
+
this.lineSpacingThreshold = config.lineSpacingThreshold ?? DEFAULT_LINE_SPACING;
|
|
8
|
+
}
|
|
9
|
+
/** Extract text content from raw text items.
|
|
10
|
+
* In Phase 2 this will accept pdf.js TextItem[]; for now it works with PdfCharInfo[]. */
|
|
11
|
+
extract(chars, _page) {
|
|
12
|
+
if (chars.length === 0) {
|
|
13
|
+
return { chars, words: [], lines: [] };
|
|
14
|
+
}
|
|
15
|
+
const words = this.segmentWords(chars);
|
|
16
|
+
const lines = this.segmentLines(words, chars);
|
|
17
|
+
return { chars, words, lines };
|
|
18
|
+
}
|
|
19
|
+
/** Build PdfCharInfo from a simple text string (for testing/placeholder). */
|
|
20
|
+
buildCharsFromString(text, startX, startY, fontSize, fontName = "Helvetica") {
|
|
21
|
+
const chars = [];
|
|
22
|
+
let x = startX;
|
|
23
|
+
const charWidth = fontSize * 0.6;
|
|
24
|
+
for (let i = 0; i < text.length; i++) {
|
|
25
|
+
const char = text[i];
|
|
26
|
+
const rect = [x, startY, x + charWidth, startY + fontSize];
|
|
27
|
+
chars.push({
|
|
28
|
+
char,
|
|
29
|
+
rect,
|
|
30
|
+
fontName,
|
|
31
|
+
fontSize,
|
|
32
|
+
transform: [fontSize, 0, 0, fontSize, x, startY]
|
|
33
|
+
});
|
|
34
|
+
x += charWidth;
|
|
35
|
+
}
|
|
36
|
+
return chars;
|
|
37
|
+
}
|
|
38
|
+
/** Segment characters into words based on spacing. */
|
|
39
|
+
segmentWords(chars) {
|
|
40
|
+
if (chars.length === 0) return [];
|
|
41
|
+
const words = [];
|
|
42
|
+
let wordStart = 0;
|
|
43
|
+
let wordChars = [chars[0]];
|
|
44
|
+
for (let i = 1; i < chars.length; i++) {
|
|
45
|
+
const prev = chars[i - 1];
|
|
46
|
+
const curr = chars[i];
|
|
47
|
+
const gap = curr.rect[0] - prev.rect[2];
|
|
48
|
+
const isSpace = curr.char === " " || prev.char === " ";
|
|
49
|
+
const isNewLine = Math.abs(curr.rect[1] - prev.rect[1]) > this.lineSpacingThreshold;
|
|
50
|
+
const isWordBreak = gap > this.wordSpacingThreshold || isSpace || isNewLine;
|
|
51
|
+
if (isWordBreak) {
|
|
52
|
+
const text = wordChars.map((c) => c.char).join("").trim();
|
|
53
|
+
if (text.length > 0) {
|
|
54
|
+
words.push({
|
|
55
|
+
text,
|
|
56
|
+
rect: this.boundingRect(wordChars),
|
|
57
|
+
charIndices: [wordStart, wordStart + wordChars.length - 1]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (curr.char !== " ") {
|
|
61
|
+
wordStart = i;
|
|
62
|
+
wordChars = [curr];
|
|
63
|
+
} else {
|
|
64
|
+
wordStart = i + 1;
|
|
65
|
+
wordChars = [];
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
wordChars.push(curr);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (wordChars.length > 0) {
|
|
72
|
+
const text = wordChars.map((c) => c.char).join("").trim();
|
|
73
|
+
if (text.length > 0) {
|
|
74
|
+
words.push({
|
|
75
|
+
text,
|
|
76
|
+
rect: this.boundingRect(wordChars),
|
|
77
|
+
charIndices: [wordStart, wordStart + wordChars.length - 1]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return words;
|
|
82
|
+
}
|
|
83
|
+
/** Segment words into lines based on vertical position. */
|
|
84
|
+
segmentLines(words, _chars) {
|
|
85
|
+
if (words.length === 0) return [];
|
|
86
|
+
const lines = [];
|
|
87
|
+
let lineStart = 0;
|
|
88
|
+
let lineWords = [words[0]];
|
|
89
|
+
let lineY = words[0].rect[1];
|
|
90
|
+
for (let i = 1; i < words.length; i++) {
|
|
91
|
+
const word = words[i];
|
|
92
|
+
const yDiff = Math.abs(word.rect[1] - lineY);
|
|
93
|
+
if (yDiff > this.lineSpacingThreshold) {
|
|
94
|
+
lines.push(this.createLine(lineWords, lineStart));
|
|
95
|
+
lineStart = i;
|
|
96
|
+
lineWords = [word];
|
|
97
|
+
lineY = word.rect[1];
|
|
98
|
+
} else {
|
|
99
|
+
lineWords.push(word);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (lineWords.length > 0) {
|
|
103
|
+
lines.push(this.createLine(lineWords, lineStart));
|
|
104
|
+
}
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
createLine(words, startIndex) {
|
|
108
|
+
const text = words.map((w) => w.text).join(" ");
|
|
109
|
+
const rects = words.map((w) => w.rect);
|
|
110
|
+
const rect = [
|
|
111
|
+
Math.min(...rects.map((r) => r[0])),
|
|
112
|
+
Math.min(...rects.map((r) => r[1])),
|
|
113
|
+
Math.max(...rects.map((r) => r[2])),
|
|
114
|
+
Math.max(...rects.map((r) => r[3]))
|
|
115
|
+
];
|
|
116
|
+
return {
|
|
117
|
+
text,
|
|
118
|
+
rect,
|
|
119
|
+
wordIndices: [startIndex, startIndex + words.length - 1]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Compute bounding rect for a set of characters. */
|
|
123
|
+
boundingRect(chars) {
|
|
124
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
125
|
+
for (const c of chars) {
|
|
126
|
+
if (c.rect[0] < minX) minX = c.rect[0];
|
|
127
|
+
if (c.rect[1] < minY) minY = c.rect[1];
|
|
128
|
+
if (c.rect[2] > maxX) maxX = c.rect[2];
|
|
129
|
+
if (c.rect[3] > maxY) maxY = c.rect[3];
|
|
130
|
+
}
|
|
131
|
+
return [minX, minY, maxX, maxY];
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// src/search-engine.ts
|
|
136
|
+
var SearchEngine = class {
|
|
137
|
+
constructor() {
|
|
138
|
+
this.textContents = /* @__PURE__ */ new Map();
|
|
139
|
+
this.lastResult = {
|
|
140
|
+
matches: [],
|
|
141
|
+
totalCount: 0,
|
|
142
|
+
activeIndex: -1
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Set the text content for a page. */
|
|
146
|
+
setPageTextContent(pageIndex, textContent) {
|
|
147
|
+
this.textContents.set(pageIndex, textContent);
|
|
148
|
+
}
|
|
149
|
+
/** Clear text content for a page. */
|
|
150
|
+
clearPageTextContent(pageIndex) {
|
|
151
|
+
this.textContents.delete(pageIndex);
|
|
152
|
+
}
|
|
153
|
+
/** Clear all cached text content. */
|
|
154
|
+
clearAll() {
|
|
155
|
+
this.textContents.clear();
|
|
156
|
+
this.lastResult = { matches: [], totalCount: 0, activeIndex: -1 };
|
|
157
|
+
}
|
|
158
|
+
/** Search for a query across all loaded pages. */
|
|
159
|
+
search(query, options = {}) {
|
|
160
|
+
if (!query) {
|
|
161
|
+
this.lastResult = { matches: [], totalCount: 0, activeIndex: -1 };
|
|
162
|
+
return this.lastResult;
|
|
163
|
+
}
|
|
164
|
+
const matches = [];
|
|
165
|
+
const sortedPages = [...this.textContents.entries()].sort(
|
|
166
|
+
([a], [b]) => a - b
|
|
167
|
+
);
|
|
168
|
+
for (const [pageIndex, textContent] of sortedPages) {
|
|
169
|
+
const pageMatches = this.searchPage(
|
|
170
|
+
pageIndex,
|
|
171
|
+
textContent,
|
|
172
|
+
query,
|
|
173
|
+
options
|
|
174
|
+
);
|
|
175
|
+
matches.push(...pageMatches);
|
|
176
|
+
}
|
|
177
|
+
this.lastResult = {
|
|
178
|
+
matches,
|
|
179
|
+
totalCount: matches.length,
|
|
180
|
+
activeIndex: matches.length > 0 ? 0 : -1
|
|
181
|
+
};
|
|
182
|
+
return this.lastResult;
|
|
183
|
+
}
|
|
184
|
+
/** Navigate to the next search match. */
|
|
185
|
+
nextMatch() {
|
|
186
|
+
if (this.lastResult.totalCount === 0) return null;
|
|
187
|
+
this.lastResult.activeIndex = (this.lastResult.activeIndex + 1) % this.lastResult.totalCount;
|
|
188
|
+
return this.lastResult.matches[this.lastResult.activeIndex] ?? null;
|
|
189
|
+
}
|
|
190
|
+
/** Navigate to the previous search match. */
|
|
191
|
+
prevMatch() {
|
|
192
|
+
if (this.lastResult.totalCount === 0) return null;
|
|
193
|
+
this.lastResult.activeIndex = (this.lastResult.activeIndex - 1 + this.lastResult.totalCount) % this.lastResult.totalCount;
|
|
194
|
+
return this.lastResult.matches[this.lastResult.activeIndex] ?? null;
|
|
195
|
+
}
|
|
196
|
+
/** Get the current active match. */
|
|
197
|
+
getActiveMatch() {
|
|
198
|
+
if (this.lastResult.activeIndex < 0 || this.lastResult.activeIndex >= this.lastResult.totalCount) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return this.lastResult.matches[this.lastResult.activeIndex] ?? null;
|
|
202
|
+
}
|
|
203
|
+
/** Get the last search result. */
|
|
204
|
+
getLastResult() {
|
|
205
|
+
return this.lastResult;
|
|
206
|
+
}
|
|
207
|
+
searchPage(pageIndex, textContent, query, options) {
|
|
208
|
+
const matches = [];
|
|
209
|
+
const fullText = textContent.lines.map((l) => l.text).join("\n");
|
|
210
|
+
let pattern;
|
|
211
|
+
try {
|
|
212
|
+
if (options.regex) {
|
|
213
|
+
const flags = options.caseSensitive ? "g" : "gi";
|
|
214
|
+
pattern = new RegExp(query, flags);
|
|
215
|
+
} else {
|
|
216
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
217
|
+
const flags = options.caseSensitive ? "g" : "gi";
|
|
218
|
+
const word = options.wholeWord ? `\\b${escaped}\\b` : escaped;
|
|
219
|
+
pattern = new RegExp(word, flags);
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
return matches;
|
|
223
|
+
}
|
|
224
|
+
let match;
|
|
225
|
+
while ((match = pattern.exec(fullText)) !== null) {
|
|
226
|
+
const matchText = match[0];
|
|
227
|
+
const matchStart = match.index;
|
|
228
|
+
const matchEnd = matchStart + matchText.length;
|
|
229
|
+
const rects = this.findMatchRects(textContent, matchStart, matchEnd);
|
|
230
|
+
matches.push({
|
|
231
|
+
pageIndex,
|
|
232
|
+
charStart: matchStart,
|
|
233
|
+
charEnd: matchEnd,
|
|
234
|
+
rects,
|
|
235
|
+
text: matchText
|
|
236
|
+
});
|
|
237
|
+
if (matchText.length === 0) {
|
|
238
|
+
pattern.lastIndex++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return matches;
|
|
242
|
+
}
|
|
243
|
+
/** Find bounding rects for a text range. */
|
|
244
|
+
findMatchRects(textContent, _start, _end) {
|
|
245
|
+
const rects = [];
|
|
246
|
+
let charOffset = 0;
|
|
247
|
+
for (const line of textContent.lines) {
|
|
248
|
+
const lineEnd = charOffset + line.text.length;
|
|
249
|
+
if (lineEnd > _start && charOffset < _end) {
|
|
250
|
+
for (let wi = line.wordIndices[0]; wi <= line.wordIndices[1]; wi++) {
|
|
251
|
+
const word = textContent.words[wi];
|
|
252
|
+
if (word) {
|
|
253
|
+
rects.push(word.rect);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
charOffset = lineEnd + 1;
|
|
258
|
+
}
|
|
259
|
+
return rects.length > 0 ? rects : [[0, 0, 0, 0]];
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/text-plugin.ts
|
|
264
|
+
var INITIAL_STATE = {
|
|
265
|
+
searchQuery: "",
|
|
266
|
+
searchOptions: {},
|
|
267
|
+
searchResult: null,
|
|
268
|
+
activeMatch: null
|
|
269
|
+
};
|
|
270
|
+
function createTextPlugin() {
|
|
271
|
+
return {
|
|
272
|
+
id: "text",
|
|
273
|
+
name: "Text Extraction & Search",
|
|
274
|
+
version: "0.1.0",
|
|
275
|
+
install(context) {
|
|
276
|
+
const extractor = new TextExtractor();
|
|
277
|
+
const searchEngine = new SearchEngine();
|
|
278
|
+
context.registerState("text", { ...INITIAL_STATE });
|
|
279
|
+
const unsubExtract = context.commandBus.registerHandler(
|
|
280
|
+
"text:extract",
|
|
281
|
+
(payload) => {
|
|
282
|
+
const state = context.store.getState();
|
|
283
|
+
const page = state.pages[payload.pageIndex];
|
|
284
|
+
if (!page) return;
|
|
285
|
+
const chars = extractor.buildCharsFromString(
|
|
286
|
+
`Sample text for page ${payload.pageIndex + 1}`,
|
|
287
|
+
72,
|
|
288
|
+
// 1 inch margin
|
|
289
|
+
72,
|
|
290
|
+
12
|
|
291
|
+
);
|
|
292
|
+
const textContent = extractor.extract(chars, page);
|
|
293
|
+
context.store.setState((prev) => {
|
|
294
|
+
const pages = [...prev.pages];
|
|
295
|
+
const existing = pages[payload.pageIndex];
|
|
296
|
+
if (existing) {
|
|
297
|
+
pages[payload.pageIndex] = { ...existing, textContent };
|
|
298
|
+
}
|
|
299
|
+
return { ...prev, pages };
|
|
300
|
+
});
|
|
301
|
+
searchEngine.setPageTextContent(payload.pageIndex, textContent);
|
|
302
|
+
context.eventBus.emit("text:extracted", {
|
|
303
|
+
pageIndex: payload.pageIndex,
|
|
304
|
+
textContent
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
const unsubSearch = context.commandBus.registerHandler(
|
|
309
|
+
"text:search",
|
|
310
|
+
(payload) => {
|
|
311
|
+
const result = searchEngine.search(payload.query, payload.options);
|
|
312
|
+
context.setState("text", (prev) => ({
|
|
313
|
+
...prev,
|
|
314
|
+
searchQuery: payload.query,
|
|
315
|
+
searchOptions: payload.options ?? {},
|
|
316
|
+
searchResult: result,
|
|
317
|
+
activeMatch: searchEngine.getActiveMatch()
|
|
318
|
+
}));
|
|
319
|
+
context.eventBus.emit("search:found", {
|
|
320
|
+
query: payload.query,
|
|
321
|
+
matches: result.matches,
|
|
322
|
+
total: result.totalCount
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
const unsubSearchNext = context.commandBus.registerHandler(
|
|
327
|
+
"text:searchNext",
|
|
328
|
+
() => {
|
|
329
|
+
const match = searchEngine.nextMatch();
|
|
330
|
+
context.setState("text", (prev) => ({
|
|
331
|
+
...prev,
|
|
332
|
+
activeMatch: match,
|
|
333
|
+
searchResult: searchEngine.getLastResult()
|
|
334
|
+
}));
|
|
335
|
+
if (match) {
|
|
336
|
+
context.api.goToPage(match.pageIndex);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
const unsubSearchPrev = context.commandBus.registerHandler(
|
|
341
|
+
"text:searchPrev",
|
|
342
|
+
() => {
|
|
343
|
+
const match = searchEngine.prevMatch();
|
|
344
|
+
context.setState("text", (prev) => ({
|
|
345
|
+
...prev,
|
|
346
|
+
activeMatch: match,
|
|
347
|
+
searchResult: searchEngine.getLastResult()
|
|
348
|
+
}));
|
|
349
|
+
if (match) {
|
|
350
|
+
context.api.goToPage(match.pageIndex);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
const pluginApi = {
|
|
355
|
+
extractPageText(pageIndex) {
|
|
356
|
+
context.commandBus.dispatch("text:extract", { pageIndex });
|
|
357
|
+
},
|
|
358
|
+
extractAllText() {
|
|
359
|
+
const state = context.store.getState();
|
|
360
|
+
for (let i = 0; i < state.pages.length; i++) {
|
|
361
|
+
context.commandBus.dispatch("text:extract", { pageIndex: i });
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
search(query, options) {
|
|
365
|
+
context.commandBus.dispatch("text:search", { query, options });
|
|
366
|
+
return searchEngine.getLastResult();
|
|
367
|
+
},
|
|
368
|
+
searchNext() {
|
|
369
|
+
context.commandBus.dispatch("text:searchNext", {});
|
|
370
|
+
return searchEngine.getActiveMatch();
|
|
371
|
+
},
|
|
372
|
+
searchPrev() {
|
|
373
|
+
context.commandBus.dispatch("text:searchPrev", {});
|
|
374
|
+
return searchEngine.getActiveMatch();
|
|
375
|
+
},
|
|
376
|
+
clearSearch() {
|
|
377
|
+
searchEngine.clearAll();
|
|
378
|
+
context.setState("text", () => ({
|
|
379
|
+
...INITIAL_STATE
|
|
380
|
+
}));
|
|
381
|
+
},
|
|
382
|
+
getExtractor() {
|
|
383
|
+
return extractor;
|
|
384
|
+
},
|
|
385
|
+
getSearchEngine() {
|
|
386
|
+
return searchEngine;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
context._pluginApi = pluginApi;
|
|
390
|
+
return () => {
|
|
391
|
+
unsubExtract();
|
|
392
|
+
unsubSearch();
|
|
393
|
+
unsubSearchNext();
|
|
394
|
+
unsubSearchPrev();
|
|
395
|
+
searchEngine.clearAll();
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export { SearchEngine, TextExtractor, createTextPlugin };
|
|
402
|
+
//# sourceMappingURL=index.js.map
|
|
403
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/text-extractor.ts","../src/search-engine.ts","../src/text-plugin.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ─── Text Extractor ───\n//\n// Extracts text content from PDF pages with character, word, and line bounding boxes.\n// Phase 1 implementation uses synthetic extraction; Phase 2 will integrate pdf.js getTextContent().\n\nimport type {\n PdfTextContent,\n PdfCharInfo,\n PdfWordInfo,\n PdfLineInfo,\n PdfRect,\n PdfPageState,\n} from '@gridstorm/pdf-core';\n\n/** Configuration for text extraction. */\nexport interface TextExtractorConfig {\n /** Threshold for word break detection (in PDF points). */\n wordSpacingThreshold?: number;\n /** Threshold for line break detection (in PDF points). */\n lineSpacingThreshold?: number;\n}\n\nconst DEFAULT_WORD_SPACING = 3;\nconst DEFAULT_LINE_SPACING = 5;\n\n/** Extracts structured text content from PDF text items. */\nexport class TextExtractor {\n private wordSpacingThreshold: number;\n private lineSpacingThreshold: number;\n\n constructor(config: TextExtractorConfig = {}) {\n this.wordSpacingThreshold = config.wordSpacingThreshold ?? DEFAULT_WORD_SPACING;\n this.lineSpacingThreshold = config.lineSpacingThreshold ?? DEFAULT_LINE_SPACING;\n }\n\n /** Extract text content from raw text items.\n * In Phase 2 this will accept pdf.js TextItem[]; for now it works with PdfCharInfo[]. */\n extract(chars: PdfCharInfo[], _page: PdfPageState): PdfTextContent {\n if (chars.length === 0) {\n return { chars, words: [], lines: [] };\n }\n\n const words = this.segmentWords(chars);\n const lines = this.segmentLines(words, chars);\n\n return { chars, words, lines };\n }\n\n /** Build PdfCharInfo from a simple text string (for testing/placeholder). */\n buildCharsFromString(\n text: string,\n startX: number,\n startY: number,\n fontSize: number,\n fontName = 'Helvetica',\n ): PdfCharInfo[] {\n const chars: PdfCharInfo[] = [];\n let x = startX;\n const charWidth = fontSize * 0.6; // Approximate monospace width\n\n for (let i = 0; i < text.length; i++) {\n const char = text[i]!;\n const rect: PdfRect = [x, startY, x + charWidth, startY + fontSize];\n\n chars.push({\n char,\n rect,\n fontName,\n fontSize,\n transform: [fontSize, 0, 0, fontSize, x, startY],\n });\n\n x += charWidth;\n }\n\n return chars;\n }\n\n /** Segment characters into words based on spacing. */\n private segmentWords(chars: PdfCharInfo[]): PdfWordInfo[] {\n if (chars.length === 0) return [];\n\n const words: PdfWordInfo[] = [];\n let wordStart = 0;\n let wordChars: PdfCharInfo[] = [chars[0]!];\n\n for (let i = 1; i < chars.length; i++) {\n const prev = chars[i - 1]!;\n const curr = chars[i]!;\n\n const gap = curr.rect[0] - prev.rect[2]; // x1 of current - x2 of previous\n const isSpace = curr.char === ' ' || prev.char === ' ';\n const isNewLine = Math.abs(curr.rect[1] - prev.rect[1]) > this.lineSpacingThreshold;\n const isWordBreak = gap > this.wordSpacingThreshold || isSpace || isNewLine;\n\n if (isWordBreak) {\n // Complete current word (skip if only spaces)\n const text = wordChars.map((c) => c.char).join('').trim();\n if (text.length > 0) {\n words.push({\n text,\n rect: this.boundingRect(wordChars),\n charIndices: [wordStart, wordStart + wordChars.length - 1],\n });\n }\n\n // Skip space characters\n if (curr.char !== ' ') {\n wordStart = i;\n wordChars = [curr];\n } else {\n wordStart = i + 1;\n wordChars = [];\n }\n } else {\n wordChars.push(curr);\n }\n }\n\n // Final word\n if (wordChars.length > 0) {\n const text = wordChars.map((c) => c.char).join('').trim();\n if (text.length > 0) {\n words.push({\n text,\n rect: this.boundingRect(wordChars),\n charIndices: [wordStart, wordStart + wordChars.length - 1],\n });\n }\n }\n\n return words;\n }\n\n /** Segment words into lines based on vertical position. */\n private segmentLines(words: PdfWordInfo[], _chars: PdfCharInfo[]): PdfLineInfo[] {\n if (words.length === 0) return [];\n\n const lines: PdfLineInfo[] = [];\n let lineStart = 0;\n let lineWords: PdfWordInfo[] = [words[0]!];\n let lineY = words[0]!.rect[1]; // y1 of first word\n\n for (let i = 1; i < words.length; i++) {\n const word = words[i]!;\n const yDiff = Math.abs(word.rect[1] - lineY);\n\n if (yDiff > this.lineSpacingThreshold) {\n // New line\n lines.push(this.createLine(lineWords, lineStart));\n lineStart = i;\n lineWords = [word];\n lineY = word.rect[1];\n } else {\n lineWords.push(word);\n }\n }\n\n // Final line\n if (lineWords.length > 0) {\n lines.push(this.createLine(lineWords, lineStart));\n }\n\n return lines;\n }\n\n private createLine(words: PdfWordInfo[], startIndex: number): PdfLineInfo {\n const text = words.map((w) => w.text).join(' ');\n const rects = words.map((w) => w.rect);\n const rect: PdfRect = [\n Math.min(...rects.map((r) => r[0])),\n Math.min(...rects.map((r) => r[1])),\n Math.max(...rects.map((r) => r[2])),\n Math.max(...rects.map((r) => r[3])),\n ];\n return {\n text,\n rect,\n wordIndices: [startIndex, startIndex + words.length - 1],\n };\n }\n\n /** Compute bounding rect for a set of characters. */\n private boundingRect(chars: PdfCharInfo[]): PdfRect {\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n\n for (const c of chars) {\n if (c.rect[0] < minX) minX = c.rect[0];\n if (c.rect[1] < minY) minY = c.rect[1];\n if (c.rect[2] > maxX) maxX = c.rect[2];\n if (c.rect[3] > maxY) maxY = c.rect[3];\n }\n\n return [minX, minY, maxX, maxY];\n }\n}\n","// ─── Search Engine ───\n//\n// Full-text search across PDF pages with match navigation.\n\nimport type { PdfTextContent, PdfRect } from '@gridstorm/pdf-core';\n\n/** Search options. */\nexport interface SearchOptions {\n /** Case-sensitive search. */\n caseSensitive?: boolean;\n /** Match whole words only. */\n wholeWord?: boolean;\n /** Use regex pattern. */\n regex?: boolean;\n}\n\n/** A single search match. */\nexport interface SearchMatch {\n /** Page index where the match was found. */\n pageIndex: number;\n /** Start character index within the page text. */\n charStart: number;\n /** End character index (exclusive). */\n charEnd: number;\n /** Bounding rectangles covering the match (may span multiple lines). */\n rects: PdfRect[];\n /** The matched text. */\n text: string;\n}\n\n/** Search result set. */\nexport interface SearchResult {\n /** All matches across all pages. */\n matches: SearchMatch[];\n /** Total match count. */\n totalCount: number;\n /** Currently active match index (-1 if none). */\n activeIndex: number;\n}\n\n/** Search engine for finding text in PDF pages. */\nexport class SearchEngine {\n private textContents = new Map<number, PdfTextContent>();\n private lastResult: SearchResult = {\n matches: [],\n totalCount: 0,\n activeIndex: -1,\n };\n\n /** Set the text content for a page. */\n setPageTextContent(pageIndex: number, textContent: PdfTextContent): void {\n this.textContents.set(pageIndex, textContent);\n }\n\n /** Clear text content for a page. */\n clearPageTextContent(pageIndex: number): void {\n this.textContents.delete(pageIndex);\n }\n\n /** Clear all cached text content. */\n clearAll(): void {\n this.textContents.clear();\n this.lastResult = { matches: [], totalCount: 0, activeIndex: -1 };\n }\n\n /** Search for a query across all loaded pages. */\n search(query: string, options: SearchOptions = {}): SearchResult {\n if (!query) {\n this.lastResult = { matches: [], totalCount: 0, activeIndex: -1 };\n return this.lastResult;\n }\n\n const matches: SearchMatch[] = [];\n\n // Sort pages by index for consistent ordering\n const sortedPages = [...this.textContents.entries()].sort(\n ([a], [b]) => a - b,\n );\n\n for (const [pageIndex, textContent] of sortedPages) {\n const pageMatches = this.searchPage(\n pageIndex,\n textContent,\n query,\n options,\n );\n matches.push(...pageMatches);\n }\n\n this.lastResult = {\n matches,\n totalCount: matches.length,\n activeIndex: matches.length > 0 ? 0 : -1,\n };\n\n return this.lastResult;\n }\n\n /** Navigate to the next search match. */\n nextMatch(): SearchMatch | null {\n if (this.lastResult.totalCount === 0) return null;\n\n this.lastResult.activeIndex =\n (this.lastResult.activeIndex + 1) % this.lastResult.totalCount;\n return this.lastResult.matches[this.lastResult.activeIndex] ?? null;\n }\n\n /** Navigate to the previous search match. */\n prevMatch(): SearchMatch | null {\n if (this.lastResult.totalCount === 0) return null;\n\n this.lastResult.activeIndex =\n (this.lastResult.activeIndex - 1 + this.lastResult.totalCount) %\n this.lastResult.totalCount;\n return this.lastResult.matches[this.lastResult.activeIndex] ?? null;\n }\n\n /** Get the current active match. */\n getActiveMatch(): SearchMatch | null {\n if (\n this.lastResult.activeIndex < 0 ||\n this.lastResult.activeIndex >= this.lastResult.totalCount\n ) {\n return null;\n }\n return this.lastResult.matches[this.lastResult.activeIndex] ?? null;\n }\n\n /** Get the last search result. */\n getLastResult(): SearchResult {\n return this.lastResult;\n }\n\n private searchPage(\n pageIndex: number,\n textContent: PdfTextContent,\n query: string,\n options: SearchOptions,\n ): SearchMatch[] {\n const matches: SearchMatch[] = [];\n\n // Build full page text from lines\n const fullText = textContent.lines.map((l) => l.text).join('\\n');\n\n let pattern: RegExp;\n try {\n if (options.regex) {\n const flags = options.caseSensitive ? 'g' : 'gi';\n pattern = new RegExp(query, flags);\n } else {\n const escaped = query.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const flags = options.caseSensitive ? 'g' : 'gi';\n const word = options.wholeWord ? `\\\\b${escaped}\\\\b` : escaped;\n pattern = new RegExp(word, flags);\n }\n } catch {\n return matches;\n }\n\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(fullText)) !== null) {\n const matchText = match[0];\n const matchStart = match.index;\n const matchEnd = matchStart + matchText.length;\n\n // Find covering word rects\n const rects = this.findMatchRects(textContent, matchStart, matchEnd);\n\n matches.push({\n pageIndex,\n charStart: matchStart,\n charEnd: matchEnd,\n rects,\n text: matchText,\n });\n\n // Avoid infinite loop for zero-length matches\n if (matchText.length === 0) {\n pattern.lastIndex++;\n }\n }\n\n return matches;\n }\n\n /** Find bounding rects for a text range. */\n private findMatchRects(\n textContent: PdfTextContent,\n _start: number,\n _end: number,\n ): PdfRect[] {\n // In Phase 1 we use word-level rects; Phase 2 will use char-level precision.\n // For now, return rects for all words that overlap the match range.\n const rects: PdfRect[] = [];\n\n // Simple approach: iterate lines and match by character offset\n let charOffset = 0;\n for (const line of textContent.lines) {\n const lineEnd = charOffset + line.text.length;\n\n if (lineEnd > _start && charOffset < _end) {\n // This line overlaps the match — use word-level rects within the line\n for (let wi = line.wordIndices[0]; wi <= line.wordIndices[1]; wi++) {\n const word = textContent.words[wi];\n if (word) {\n rects.push(word.rect);\n }\n }\n }\n\n charOffset = lineEnd + 1; // +1 for \\n\n }\n\n return rects.length > 0 ? rects : [[0, 0, 0, 0]];\n }\n}\n","// ─── Text Plugin ───\n//\n// GridStorm PDF plugin for text extraction and search.\n\nimport type {\n PdfPlugin,\n PdfPluginContext,\n PdfPluginDisposer,\n} from '@gridstorm/pdf-core';\nimport { TextExtractor } from './text-extractor';\nimport { SearchEngine } from './search-engine';\nimport type { SearchOptions, SearchResult, SearchMatch } from './search-engine';\n\n/** Plugin state for text operations. */\nexport interface TextPluginState {\n /** Current search query. */\n searchQuery: string;\n /** Search options. */\n searchOptions: SearchOptions;\n /** Last search result. */\n searchResult: SearchResult | null;\n /** Active search match. */\n activeMatch: SearchMatch | null;\n}\n\n/** Public API exposed by the text plugin. */\nexport interface TextPluginApi {\n /** Extract text from a specific page. */\n extractPageText(pageIndex: number): void;\n /** Extract text from all pages. */\n extractAllText(): void;\n /** Search for text across all pages. */\n search(query: string, options?: SearchOptions): SearchResult;\n /** Navigate to next search match. */\n searchNext(): SearchMatch | null;\n /** Navigate to previous search match. */\n searchPrev(): SearchMatch | null;\n /** Clear search results. */\n clearSearch(): void;\n /** Get the text extractor instance. */\n getExtractor(): TextExtractor;\n /** Get the search engine instance. */\n getSearchEngine(): SearchEngine;\n}\n\nconst INITIAL_STATE: TextPluginState = {\n searchQuery: '',\n searchOptions: {},\n searchResult: null,\n activeMatch: null,\n};\n\n/** Create the text extraction and search plugin. */\nexport function createTextPlugin(): PdfPlugin {\n return {\n id: 'text',\n name: 'Text Extraction & Search',\n version: '0.1.0',\n\n install(context: PdfPluginContext): PdfPluginDisposer {\n const extractor = new TextExtractor();\n const searchEngine = new SearchEngine();\n\n // Register plugin state\n context.registerState<TextPluginState>('text', { ...INITIAL_STATE });\n\n // ─── Command Handlers ───\n\n const unsubExtract = context.commandBus.registerHandler(\n 'text:extract',\n (payload: { pageIndex: number }) => {\n const state = context.store.getState();\n const page = state.pages[payload.pageIndex];\n if (!page) return;\n\n // In Phase 1, use placeholder chars; Phase 2 will use pdf.js\n const chars = extractor.buildCharsFromString(\n `Sample text for page ${payload.pageIndex + 1}`,\n 72, // 1 inch margin\n 72,\n 12,\n );\n\n const textContent = extractor.extract(chars, page);\n\n // Update page state with extracted text\n context.store.setState((prev) => {\n const pages = [...prev.pages];\n const existing = pages[payload.pageIndex];\n if (existing) {\n pages[payload.pageIndex] = { ...existing, textContent };\n }\n return { ...prev, pages };\n });\n\n // Feed text to search engine\n searchEngine.setPageTextContent(payload.pageIndex, textContent);\n\n context.eventBus.emit('text:extracted', {\n pageIndex: payload.pageIndex,\n textContent,\n });\n },\n );\n\n const unsubSearch = context.commandBus.registerHandler(\n 'text:search',\n (payload: { query: string; options?: SearchOptions }) => {\n const result = searchEngine.search(payload.query, payload.options);\n\n context.setState<TextPluginState>('text', (prev) => ({\n ...prev,\n searchQuery: payload.query,\n searchOptions: payload.options ?? {},\n searchResult: result,\n activeMatch: searchEngine.getActiveMatch(),\n }));\n\n context.eventBus.emit('search:found', {\n query: payload.query,\n matches: result.matches,\n total: result.totalCount,\n });\n },\n );\n\n const unsubSearchNext = context.commandBus.registerHandler(\n 'text:searchNext',\n () => {\n const match = searchEngine.nextMatch();\n context.setState<TextPluginState>('text', (prev) => ({\n ...prev,\n activeMatch: match,\n searchResult: searchEngine.getLastResult(),\n }));\n\n if (match) {\n // Navigate to the match page\n context.api.goToPage(match.pageIndex);\n }\n },\n );\n\n const unsubSearchPrev = context.commandBus.registerHandler(\n 'text:searchPrev',\n () => {\n const match = searchEngine.prevMatch();\n context.setState<TextPluginState>('text', (prev) => ({\n ...prev,\n activeMatch: match,\n searchResult: searchEngine.getLastResult(),\n }));\n\n if (match) {\n context.api.goToPage(match.pageIndex);\n }\n },\n );\n\n // ─── Plugin API ───\n\n const pluginApi: TextPluginApi = {\n extractPageText(pageIndex: number) {\n context.commandBus.dispatch('text:extract', { pageIndex });\n },\n extractAllText() {\n const state = context.store.getState();\n for (let i = 0; i < state.pages.length; i++) {\n context.commandBus.dispatch('text:extract', { pageIndex: i });\n }\n },\n search(query: string, options?: SearchOptions) {\n context.commandBus.dispatch('text:search', { query, options });\n return searchEngine.getLastResult();\n },\n searchNext() {\n context.commandBus.dispatch('text:searchNext', {});\n return searchEngine.getActiveMatch();\n },\n searchPrev() {\n context.commandBus.dispatch('text:searchPrev', {});\n return searchEngine.getActiveMatch();\n },\n clearSearch() {\n searchEngine.clearAll();\n context.setState<TextPluginState>('text', () => ({\n ...INITIAL_STATE,\n }));\n },\n getExtractor() {\n return extractor;\n },\n getSearchEngine() {\n return searchEngine;\n },\n };\n\n // Expose API via plugin context (can be retrieved via api.getPluginApi('text'))\n (context as any)._pluginApi = pluginApi;\n\n return () => {\n unsubExtract();\n unsubSearch();\n unsubSearchNext();\n unsubSearchPrev();\n searchEngine.clearAll();\n };\n },\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gridstorm/pdf-plugin-text",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Text extraction and search plugin for GridStorm PDF",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@gridstorm/pdf-core": "workspace:*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.5.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/007krcs/grid-data.git",
|
|
37
|
+
"directory": "packages/pdf-plugin-text"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://grid-data-analytics-explorer.vercel.app/",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/007krcs/grid-data/issues"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"sideEffects": false
|
|
47
|
+
}
|