@rightkit/document-core 0.1.1 → 0.2.0
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/CHANGELOG.md +29 -0
- package/README.md +31 -7
- package/dist/app/chrome.js +1 -1
- package/dist/app/documentPrinting.d.ts +0 -6
- package/dist/app/documentPrinting.js +0 -18
- package/dist/app/shell/nativeMenu.d.ts +3 -2
- package/dist/app/shell/nativeMenu.js +8 -5
- package/dist/documents/perfEventCounters.d.ts +0 -8
- package/dist/documents/perfEventCounters.js +2 -1
- package/dist/documents/preview/previewFrame.d.ts +2 -1
- package/dist/documents/preview/previewFrame.js +9 -7
- package/dist/documents/searchUtils.js +1 -1
- package/dist/host.d.ts +36 -0
- package/dist/host.js +54 -0
- package/dist/index.d.ts +1 -50
- package/dist/index.js +1 -50
- package/dist/settings/themes.js +1 -1
- package/dist/state/bridgeEvents.d.ts +1 -1
- package/dist/state/bridgeEvents.js +7 -4
- package/dist/state/recentUrls.js +4 -3
- package/dist/state/tauriBridge.d.ts +3 -61
- package/dist/state/tauriBridge.js +8 -297
- package/dist/styles.css +2 -2
- package/package.json +25 -98
- package/dist/app/shortcutChords.d.ts +0 -6
- package/dist/app/shortcutChords.js +0 -14
- package/dist/app/useCloseFindOnSurfaceChange.d.ts +0 -14
- package/dist/app/useCloseFindOnSurfaceChange.js +0 -23
- package/dist/app/useEditText.d.ts +0 -6
- package/dist/app/useEditText.js +0 -10
- package/dist/appName.d.ts +0 -2
- package/dist/appName.js +0 -8
- package/dist/documents/FullscreenIcon.d.ts +0 -4
- package/dist/documents/FullscreenIcon.js +0 -7
- package/dist/documents/mdx/mdxRuntimeScript.d.ts +0 -2
- package/dist/documents/mdx/mdxRuntimeScript.js +0 -3
- package/dist/documents/mdx/mdxRuntimeScript.txt +0 -584
- package/dist/documents/pdf/MacPdfReadPane.d.ts +0 -23
- package/dist/documents/pdf/MacPdfReadPane.js +0 -708
- package/dist/documents/pdf/NativePdfDocument.d.ts +0 -9
- package/dist/documents/pdf/NativePdfDocument.js +0 -41
- package/dist/documents/pdf/PdfInlineTextEditor.d.ts +0 -53
- package/dist/documents/pdf/PdfInlineTextEditor.js +0 -225
- package/dist/documents/pdf/PdfOcrLayer.d.ts +0 -4
- package/dist/documents/pdf/PdfOcrLayer.js +0 -30
- package/dist/documents/pdf/PdfOutline.d.ts +0 -6
- package/dist/documents/pdf/PdfOutline.js +0 -12
- package/dist/documents/pdf/PdfPasswordPrompt.d.ts +0 -5
- package/dist/documents/pdf/PdfPasswordPrompt.js +0 -24
- package/dist/documents/pdf/PdfThumbnails.d.ts +0 -8
- package/dist/documents/pdf/PdfThumbnails.js +0 -201
- package/dist/documents/pdf/PdfToolbar.d.ts +0 -47
- package/dist/documents/pdf/PdfToolbar.js +0 -76
- package/dist/documents/pdf/PdfViewer.d.ts +0 -36
- package/dist/documents/pdf/PdfViewer.js +0 -1522
- package/dist/documents/pdf/PdfViewerBody.d.ts +0 -22
- package/dist/documents/pdf/PdfViewerBody.js +0 -42
- package/dist/documents/pdf/macPdfNativeBridge.d.ts +0 -118
- package/dist/documents/pdf/macPdfNativeBridge.js +0 -134
- package/dist/documents/pdf/nearestVisiblePdfPage.d.ts +0 -2
- package/dist/documents/pdf/nearestVisiblePdfPage.js +0 -22
- package/dist/documents/pdf/pdfBench.d.ts +0 -88
- package/dist/documents/pdf/pdfBench.js +0 -535
- package/dist/documents/pdf/pdfBenchAutopilot.d.ts +0 -48
- package/dist/documents/pdf/pdfBenchAutopilot.js +0 -154
- package/dist/documents/pdf/pdfBenchTransport.d.ts +0 -21
- package/dist/documents/pdf/pdfBenchTransport.js +0 -233
- package/dist/documents/pdf/pdfEditModel.d.ts +0 -294
- package/dist/documents/pdf/pdfEditModel.js +0 -105
- package/dist/documents/pdf/pdfEditor.d.ts +0 -100
- package/dist/documents/pdf/pdfEditor.js +0 -68
- package/dist/documents/pdf/pdfOcr.d.ts +0 -36
- package/dist/documents/pdf/pdfOcr.js +0 -88
- package/dist/documents/pdf/pdfOcrEdit.d.ts +0 -32
- package/dist/documents/pdf/pdfOcrEdit.js +0 -138
- package/dist/documents/pdf/pdfPrinting.d.ts +0 -28
- package/dist/documents/pdf/pdfPrinting.js +0 -110
- package/dist/documents/pdf/pdfSave.d.ts +0 -87
- package/dist/documents/pdf/pdfSave.js +0 -250
- package/dist/documents/pdf/pdfShared.d.ts +0 -16
- package/dist/documents/pdf/pdfShared.js +0 -20
- package/dist/documents/pdf/pdfToMarkdown.d.ts +0 -11
- package/dist/documents/pdf/pdfToMarkdown.js +0 -67
- package/dist/documents/pdf/pdfjsLibGlobal.d.ts +0 -1
- package/dist/documents/pdf/pdfjsLibGlobal.js +0 -12
- package/dist/documents/pdf/toolbarIcons.d.ts +0 -18
- package/dist/documents/pdf/toolbarIcons.js +0 -31
- package/dist/documents/pdf/usePdfDocumentLoader.d.ts +0 -40
- package/dist/documents/pdf/usePdfDocumentLoader.js +0 -243
- package/dist/documents/pdf/usePdfEditSession.d.ts +0 -88
- package/dist/documents/pdf/usePdfEditSession.js +0 -341
- package/dist/documents/pdf/usePdfEditor.d.ts +0 -59
- package/dist/documents/pdf/usePdfEditor.js +0 -317
- package/dist/documents/pdf/usePdfFit.d.ts +0 -19
- package/dist/documents/pdf/usePdfFit.js +0 -55
- package/dist/documents/pdf/usePdfOcr.d.ts +0 -38
- package/dist/documents/pdf/usePdfOcr.js +0 -337
- package/dist/documents/pdf/usePdfPreviewRecompose.d.ts +0 -28
- package/dist/documents/pdf/usePdfPreviewRecompose.js +0 -256
- package/dist/documents/pdf/usePdfSavePlan.d.ts +0 -61
- package/dist/documents/pdf/usePdfSavePlan.js +0 -303
- package/dist/documents/pdf/usePdfSearch.d.ts +0 -31
- package/dist/documents/pdf/usePdfSearch.js +0 -367
- package/dist/documents/pdf/usePdfViewControls.d.ts +0 -72
- package/dist/documents/pdf/usePdfViewControls.js +0 -152
- package/dist/documents/scopedSelectAll.d.ts +0 -5
- package/dist/documents/scopedSelectAll.js +0 -77
- package/dist/documents/txt/cmScroll.d.ts +0 -11
- package/dist/documents/txt/cmScroll.js +0 -52
- package/dist/documents/useMirroredEditText.d.ts +0 -15
- package/dist/documents/useMirroredEditText.js +0 -29
- package/dist/documents/useSplitResize.d.ts +0 -7
- package/dist/documents/useSplitResize.js +0 -37
- package/dist/settings/rk-settings.css +0 -38
- package/dist/settings/sections/EditorSection.d.ts +0 -1
- package/dist/settings/sections/EditorSection.js +0 -18
- package/dist/settings/sections/FileAssociationsSection.d.ts +0 -1
- package/dist/settings/sections/FileAssociationsSection.js +0 -5
- package/dist/settings/sections/MacFileAssociationsBlock.d.ts +0 -33
- package/dist/settings/sections/MacFileAssociationsBlock.js +0 -151
- package/dist/settings/sections/ThemeSection.d.ts +0 -1
- package/dist/settings/sections/ThemeSection.js +0 -31
- package/dist/settings/sections/TypographySection.d.ts +0 -1
- package/dist/settings/sections/TypographySection.js +0 -19
- package/dist/settings/sections/onOff.d.ts +0 -11
- package/dist/settings/sections/onOff.js +0 -8
- package/dist/state/browserQa.d.ts +0 -21
- package/dist/state/browserQa.js +0 -303
- package/dist/state/browserQaDocx.d.ts +0 -3
- package/dist/state/browserQaDocx.js +0 -35
- package/dist/state/browserQaLicense.d.ts +0 -8
- package/dist/state/browserQaLicense.js +0 -7
- package/dist/state/browserQaPdf.d.ts +0 -60
- package/dist/state/browserQaPdf.js +0 -302
- package/dist/state/browserQaText.d.ts +0 -6
- package/dist/state/browserQaText.js +0 -48
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { isBackendUnavailable, isProbablyScannedPage, isTextlessPage, ocrBackendMissing, ocrPage, renderPageToPng, } from "./pdfOcr.js";
|
|
3
|
-
import { viewportRotation } from "./pdfShared.js";
|
|
4
|
-
import { logError, logPerf } from "../../state/diagnostics.js";
|
|
5
|
-
function errorMessage(error) {
|
|
6
|
-
if (error instanceof Error && error.message)
|
|
7
|
-
return error.message;
|
|
8
|
-
return typeof error === "string" ? error : String(error);
|
|
9
|
-
}
|
|
10
|
-
export function usePdfOcr({ documentId, pdfDoc, pdfRef, current, rotation, setPageText, invalidatePageText, rehighlight, }) {
|
|
11
|
-
const [scannedPages, setScannedPages] = useState(new Map());
|
|
12
|
-
const [pageStates, setPageStates] = useState(new Map());
|
|
13
|
-
const [linesByPage, setLinesByPage] = useState(new Map());
|
|
14
|
-
const [currentPageScanned, setCurrentPageScanned] = useState(false);
|
|
15
|
-
const [documentState, setDocumentState] = useState({
|
|
16
|
-
status: "idle",
|
|
17
|
-
completed: 0,
|
|
18
|
-
total: 0,
|
|
19
|
-
scanned: 0,
|
|
20
|
-
skipped: 0,
|
|
21
|
-
failed: 0,
|
|
22
|
-
});
|
|
23
|
-
const generationRef = useRef(0);
|
|
24
|
-
const scanRequestRef = useRef(0);
|
|
25
|
-
const pageRunRef = useRef(new Map());
|
|
26
|
-
const documentRunRef = useRef(null);
|
|
27
|
-
const previousRotationRef = useRef(rotation);
|
|
28
|
-
const linesByPageRef = useRef(linesByPage);
|
|
29
|
-
const scannedPagesRef = useRef(scannedPages);
|
|
30
|
-
const pageStatesRef = useRef(pageStates);
|
|
31
|
-
const documentStateRef = useRef(documentState);
|
|
32
|
-
linesByPageRef.current = linesByPage;
|
|
33
|
-
scannedPagesRef.current = scannedPages;
|
|
34
|
-
pageStatesRef.current = pageStates;
|
|
35
|
-
documentStateRef.current = documentState;
|
|
36
|
-
// documentId is the lifecycle boundary; reset OCR state when it changes.
|
|
37
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: documentId intentionally marks document lifecycle.
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
generationRef.current += 1;
|
|
40
|
-
scanRequestRef.current += 1;
|
|
41
|
-
pageRunRef.current.clear();
|
|
42
|
-
documentRunRef.current = null;
|
|
43
|
-
setScannedPages(new Map());
|
|
44
|
-
setPageStates(new Map());
|
|
45
|
-
setLinesByPage(new Map());
|
|
46
|
-
setCurrentPageScanned(false);
|
|
47
|
-
setDocumentState({
|
|
48
|
-
status: "idle",
|
|
49
|
-
completed: 0,
|
|
50
|
-
total: 0,
|
|
51
|
-
scanned: 0,
|
|
52
|
-
skipped: 0,
|
|
53
|
-
failed: 0,
|
|
54
|
-
});
|
|
55
|
-
return () => {
|
|
56
|
-
generationRef.current += 1;
|
|
57
|
-
scanRequestRef.current += 1;
|
|
58
|
-
pageRunRef.current.clear();
|
|
59
|
-
documentRunRef.current = null;
|
|
60
|
-
};
|
|
61
|
-
}, [documentId]);
|
|
62
|
-
// Geometry from a previous rotation is no longer valid. Do not transform it: the backend's
|
|
63
|
-
// normalized coordinates describe the raster orientation, so recognition must run again.
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
if (previousRotationRef.current === rotation)
|
|
66
|
-
return;
|
|
67
|
-
previousRotationRef.current = rotation;
|
|
68
|
-
generationRef.current += 1;
|
|
69
|
-
scanRequestRef.current += 1;
|
|
70
|
-
pageRunRef.current.clear();
|
|
71
|
-
documentRunRef.current = null;
|
|
72
|
-
const recognizedPages = [...linesByPageRef.current.keys()];
|
|
73
|
-
for (const page of recognizedPages)
|
|
74
|
-
invalidatePageText(page);
|
|
75
|
-
if (recognizedPages.length > 0) {
|
|
76
|
-
setLinesByPage((previous) => {
|
|
77
|
-
const next = new Map(previous);
|
|
78
|
-
for (const page of recognizedPages)
|
|
79
|
-
next.delete(page);
|
|
80
|
-
return next;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
setPageStates((previous) => {
|
|
84
|
-
const next = new Map(previous);
|
|
85
|
-
for (const [page, state] of next) {
|
|
86
|
-
if (state.status === "done" || state.status === "running") {
|
|
87
|
-
next.set(page, { status: "idle" });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return next;
|
|
91
|
-
});
|
|
92
|
-
setDocumentState({ status: "idle", completed: 0, total: 0, scanned: 0, skipped: 0, failed: 0 });
|
|
93
|
-
}, [invalidatePageText, rotation]);
|
|
94
|
-
// Browsing detection reads only streamed text. Image/operator inspection is deferred until an
|
|
95
|
-
// explicit OCR request, so opening/changing pages never decodes a page just to show a button.
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (!pdfDoc || current < 1 || current > pdfDoc.numPages) {
|
|
98
|
-
setCurrentPageScanned(false);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const known = scannedPagesRef.current.get(current);
|
|
102
|
-
if (known !== undefined) {
|
|
103
|
-
setCurrentPageScanned(known);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const request = ++scanRequestRef.current;
|
|
107
|
-
let cancelled = false;
|
|
108
|
-
void pdfDoc
|
|
109
|
-
.getPage(current)
|
|
110
|
-
.then((page) => isTextlessPage(page))
|
|
111
|
-
.then((textless) => {
|
|
112
|
-
if (cancelled || request !== scanRequestRef.current)
|
|
113
|
-
return;
|
|
114
|
-
setScannedPages((previous) => {
|
|
115
|
-
const next = new Map(previous);
|
|
116
|
-
next.set(current, textless);
|
|
117
|
-
return next;
|
|
118
|
-
});
|
|
119
|
-
setCurrentPageScanned(textless);
|
|
120
|
-
})
|
|
121
|
-
.catch(() => {
|
|
122
|
-
if (!cancelled && request === scanRequestRef.current)
|
|
123
|
-
setCurrentPageScanned(false);
|
|
124
|
-
});
|
|
125
|
-
return () => {
|
|
126
|
-
cancelled = true;
|
|
127
|
-
};
|
|
128
|
-
}, [current, pdfDoc]);
|
|
129
|
-
const setState = useCallback((page, state) => {
|
|
130
|
-
setPageStates((previous) => {
|
|
131
|
-
const next = new Map(previous);
|
|
132
|
-
next.set(page, state);
|
|
133
|
-
return next;
|
|
134
|
-
});
|
|
135
|
-
}, []);
|
|
136
|
-
const isCurrentGeneration = useCallback((generation, pdf) => generation === generationRef.current && pdfRef.current === pdf, [pdfRef]);
|
|
137
|
-
const recognizePage = useCallback(async (pageNumber, pdf, generation, pageOverride) => {
|
|
138
|
-
const startedAt = performance.now();
|
|
139
|
-
setState(pageNumber, { status: "running" });
|
|
140
|
-
try {
|
|
141
|
-
const page = pageOverride ?? (await pdf.getPage(pageNumber));
|
|
142
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
143
|
-
return "cancelled";
|
|
144
|
-
const png = await renderPageToPng(page, 300, viewportRotation(page.rotate, rotation));
|
|
145
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
146
|
-
return "cancelled";
|
|
147
|
-
const lines = await ocrPage(new Uint8Array(await png.arrayBuffer()));
|
|
148
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
149
|
-
return "cancelled";
|
|
150
|
-
setLinesByPage((previous) => new Map(previous).set(pageNumber, lines));
|
|
151
|
-
setState(pageNumber, { status: "done" });
|
|
152
|
-
setPageText(pageNumber, lines.map((line) => line.text).join(" "));
|
|
153
|
-
logPerf("pdf:ocr-page", startedAt);
|
|
154
|
-
rehighlight();
|
|
155
|
-
return "done";
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
159
|
-
return "cancelled";
|
|
160
|
-
setState(pageNumber, { status: "error", error: errorMessage(error) });
|
|
161
|
-
logError("pdf:ocr-page-failed");
|
|
162
|
-
return "failed";
|
|
163
|
-
}
|
|
164
|
-
}, [isCurrentGeneration, rehighlight, rotation, setPageText, setState]);
|
|
165
|
-
const recognizeCurrentPage = useCallback(async (inspectEligibility = false) => {
|
|
166
|
-
const pageNumber = current;
|
|
167
|
-
const pdf = pdfRef.current ?? pdfDoc;
|
|
168
|
-
if (!pdf ||
|
|
169
|
-
(!currentPageScanned && !inspectEligibility) ||
|
|
170
|
-
pageStatesRef.current.get(pageNumber)?.status === "running" ||
|
|
171
|
-
pageRunRef.current.size > 0 ||
|
|
172
|
-
documentRunRef.current !== null) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const generation = generationRef.current;
|
|
176
|
-
pageRunRef.current.set(pageNumber, generation);
|
|
177
|
-
setState(pageNumber, { status: "running" });
|
|
178
|
-
try {
|
|
179
|
-
const page = await pdf.getPage(pageNumber);
|
|
180
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
181
|
-
return;
|
|
182
|
-
const scanned = await isProbablyScannedPage(page);
|
|
183
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
184
|
-
return;
|
|
185
|
-
setScannedPages((previous) => new Map(previous).set(pageNumber, scanned));
|
|
186
|
-
setCurrentPageScanned(scanned);
|
|
187
|
-
if (!scanned) {
|
|
188
|
-
setState(pageNumber, { status: "idle" });
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
await recognizePage(pageNumber, pdf, generation, page);
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
195
|
-
return;
|
|
196
|
-
setState(pageNumber, { status: "error", error: "Unable to inspect page for OCR." });
|
|
197
|
-
logError("pdf:ocr-page-failed");
|
|
198
|
-
}
|
|
199
|
-
finally {
|
|
200
|
-
if (pageRunRef.current.get(pageNumber) === generation)
|
|
201
|
-
pageRunRef.current.delete(pageNumber);
|
|
202
|
-
}
|
|
203
|
-
}, [current, currentPageScanned, isCurrentGeneration, pdfDoc, pdfRef, recognizePage, setState]);
|
|
204
|
-
const recognizeDocument = useCallback(async () => {
|
|
205
|
-
const pdf = pdfRef.current ?? pdfDoc;
|
|
206
|
-
if (!pdf ||
|
|
207
|
-
documentStateRef.current.status === "running" ||
|
|
208
|
-
documentRunRef.current !== null ||
|
|
209
|
-
pageRunRef.current.size > 0) {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const generation = generationRef.current;
|
|
213
|
-
const total = pdf.numPages;
|
|
214
|
-
documentRunRef.current = generation;
|
|
215
|
-
setDocumentState({
|
|
216
|
-
status: "running",
|
|
217
|
-
completed: 0,
|
|
218
|
-
total,
|
|
219
|
-
scanned: 0,
|
|
220
|
-
skipped: 0,
|
|
221
|
-
failed: 0,
|
|
222
|
-
currentPage: 1,
|
|
223
|
-
});
|
|
224
|
-
const startedAt = performance.now();
|
|
225
|
-
let failedCount = 0;
|
|
226
|
-
try {
|
|
227
|
-
for (let pageNumber = 1; pageNumber <= total; pageNumber += 1) {
|
|
228
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
229
|
-
return;
|
|
230
|
-
const previous = pageStatesRef.current.get(pageNumber);
|
|
231
|
-
try {
|
|
232
|
-
const page = await pdf.getPage(pageNumber);
|
|
233
|
-
const scanned = await isProbablyScannedPage(page);
|
|
234
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
235
|
-
return;
|
|
236
|
-
setScannedPages((pages) => new Map(pages).set(pageNumber, scanned));
|
|
237
|
-
if (scanned) {
|
|
238
|
-
setDocumentState((state) => ({
|
|
239
|
-
...state,
|
|
240
|
-
scanned: state.scanned + 1,
|
|
241
|
-
currentPage: pageNumber,
|
|
242
|
-
}));
|
|
243
|
-
}
|
|
244
|
-
if (!scanned || previous?.status === "done") {
|
|
245
|
-
setDocumentState((state) => ({
|
|
246
|
-
...state,
|
|
247
|
-
completed: state.completed + 1,
|
|
248
|
-
skipped: state.skipped + 1,
|
|
249
|
-
currentPage: pageNumber,
|
|
250
|
-
}));
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const outcome = await recognizePage(pageNumber, pdf, generation, page);
|
|
254
|
-
if (outcome === "cancelled")
|
|
255
|
-
return;
|
|
256
|
-
if (outcome === "failed")
|
|
257
|
-
failedCount += 1;
|
|
258
|
-
setDocumentState((state) => ({
|
|
259
|
-
...state,
|
|
260
|
-
completed: state.completed + 1,
|
|
261
|
-
failed: state.failed + (outcome === "failed" ? 1 : 0),
|
|
262
|
-
currentPage: pageNumber,
|
|
263
|
-
}));
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
267
|
-
return;
|
|
268
|
-
failedCount += 1;
|
|
269
|
-
setState(pageNumber, { status: "error", error: "Unable to inspect page for OCR." });
|
|
270
|
-
logError("pdf:ocr-page-failed");
|
|
271
|
-
setDocumentState((state) => ({
|
|
272
|
-
...state,
|
|
273
|
-
completed: state.completed + 1,
|
|
274
|
-
failed: state.failed + 1,
|
|
275
|
-
currentPage: pageNumber,
|
|
276
|
-
}));
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (!isCurrentGeneration(generation, pdf))
|
|
280
|
-
return;
|
|
281
|
-
setDocumentState((state) => ({ ...state, status: failedCount > 0 ? "error" : "done" }));
|
|
282
|
-
logPerf("pdf:ocr-document", startedAt);
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
if (isCurrentGeneration(generation, pdf)) {
|
|
286
|
-
setDocumentState((state) => ({ ...state, status: "error" }));
|
|
287
|
-
setState(current, {
|
|
288
|
-
status: "error",
|
|
289
|
-
error: "Text recognition is unavailable. Try again.",
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
finally {
|
|
294
|
-
if (documentRunRef.current === generation)
|
|
295
|
-
documentRunRef.current = null;
|
|
296
|
-
}
|
|
297
|
-
}, [current, isCurrentGeneration, pdfDoc, pdfRef, recognizePage, setState]);
|
|
298
|
-
// State updates commit the OCR layer after recognition returns. Re-run once the lines are
|
|
299
|
-
// mounted so a query that was already committed can mark the newly-rendered spans.
|
|
300
|
-
useEffect(() => {
|
|
301
|
-
if (linesByPage.size > 0)
|
|
302
|
-
rehighlight();
|
|
303
|
-
}, [linesByPage, rehighlight]);
|
|
304
|
-
// Ask the backend only once a scanned page is in view, which is the first moment OCR would be
|
|
305
|
-
// offered: on macOS the probe runs a recognition canary that is costly on a cold model cache.
|
|
306
|
-
// Until it answers, and whenever it cannot answer, OCR stays offered.
|
|
307
|
-
const [ocrMissing, setOcrMissing] = useState(false);
|
|
308
|
-
useEffect(() => {
|
|
309
|
-
if (!currentPageScanned)
|
|
310
|
-
return;
|
|
311
|
-
let cancelled = false;
|
|
312
|
-
void ocrBackendMissing().then((missing) => {
|
|
313
|
-
if (!cancelled && missing)
|
|
314
|
-
setOcrMissing(true);
|
|
315
|
-
});
|
|
316
|
-
return () => {
|
|
317
|
-
cancelled = true;
|
|
318
|
-
};
|
|
319
|
-
}, [currentPageScanned]);
|
|
320
|
-
const currentPageState = pageStates.get(current) ?? { status: "idle" };
|
|
321
|
-
const currentError = currentPageState.status === "error" ? currentPageState.error : undefined;
|
|
322
|
-
return {
|
|
323
|
-
// The backend probe decides whether OCR is offered at all; PdfToolbar hides the controls when
|
|
324
|
-
// it is false. Recognition failures on the requested page are still reported from that page.
|
|
325
|
-
ocrAvailable: !ocrMissing,
|
|
326
|
-
availabilityResolved: true,
|
|
327
|
-
currentPageScanned,
|
|
328
|
-
currentPageState,
|
|
329
|
-
currentError,
|
|
330
|
-
currentErrorIsBackendUnavailable: currentError ? isBackendUnavailable(currentError) : false,
|
|
331
|
-
linesByPage,
|
|
332
|
-
pageStates,
|
|
333
|
-
recognizeCurrentPage,
|
|
334
|
-
documentOcrState: documentState,
|
|
335
|
-
recognizeDocument,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { type MutableRefObject } from "react";
|
|
2
|
-
import * as pdfjs from "pdfjs-dist";
|
|
3
|
-
import type { PDFDocumentProxy } from "pdfjs-dist";
|
|
4
|
-
import type { usePdfEditSession } from "./usePdfEditSession.js";
|
|
5
|
-
interface PdfPreviewRecomposeDeps {
|
|
6
|
-
documentPath: string;
|
|
7
|
-
editSession: ReturnType<typeof usePdfEditSession>;
|
|
8
|
-
/** Best-known document_sha256 (§5.1) — populated from every inspected page graph. */
|
|
9
|
-
sourceShaRef: MutableRefObject<string | null>;
|
|
10
|
-
/** Guards against concurrent publishes racing: a superseded call discards its own task. */
|
|
11
|
-
previewSeqRef: MutableRefObject<number>;
|
|
12
|
-
previewLoadingTaskRef: MutableRefObject<ReturnType<typeof pdfjs.getDocument> | null>;
|
|
13
|
-
previewDocRef: MutableRefObject<PDFDocumentProxy | null>;
|
|
14
|
-
previewPagesRef: MutableRefObject<Set<number>>;
|
|
15
|
-
renderedRef: MutableRefObject<Map<number, () => void>>;
|
|
16
|
-
/** Indirection to the per-page renderer so this path never grows renderPage's dep list. */
|
|
17
|
-
renderPageRef: MutableRefObject<(n: number) => Promise<void>>;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* §9.4 in-memory candidate preview: recompose the candidate document from the committed-but-unsaved
|
|
21
|
-
* source commands and re-render each affected page on every source-history mutation (commit, undo,
|
|
22
|
-
* redo — anything that calls `editSession.publish()`). Extracted from PdfViewer verbatim — no
|
|
23
|
-
* behavior change.
|
|
24
|
-
*/
|
|
25
|
-
export declare function usePdfPreviewRecompose({ documentPath, editSession, sourceShaRef, previewSeqRef, previewLoadingTaskRef, previewDocRef, previewPagesRef, renderedRef, renderPageRef, }: PdfPreviewRecomposeDeps): {
|
|
26
|
-
invalidatePreview: () => void;
|
|
27
|
-
};
|
|
28
|
-
export {};
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
-
import * as pdfjs from "pdfjs-dist";
|
|
3
|
-
import { pdfDocumentInit, pdfWasmUrl } from "@rightkit/pdf-ui";
|
|
4
|
-
import { recomposePdfPreview } from "./pdfSave.js";
|
|
5
|
-
import { composePdfPreview } from "../../state/tauriBridge.js";
|
|
6
|
-
import { logError } from "../../state/diagnostics.js";
|
|
7
|
-
import { acquireResource } from "../../app/resourceCensus.js";
|
|
8
|
-
/**
|
|
9
|
-
* §9.4 in-memory candidate preview: recompose the candidate document from the committed-but-unsaved
|
|
10
|
-
* source commands and re-render each affected page on every source-history mutation (commit, undo,
|
|
11
|
-
* redo — anything that calls `editSession.publish()`). Extracted from PdfViewer verbatim — no
|
|
12
|
-
* behavior change.
|
|
13
|
-
*/
|
|
14
|
-
export function usePdfPreviewRecompose({ documentPath, editSession, sourceShaRef, previewSeqRef, previewLoadingTaskRef, previewDocRef, previewPagesRef, renderedRef, renderPageRef, }) {
|
|
15
|
-
const onPublish = editSession.onPublish;
|
|
16
|
-
// Mirrors editSession.session.sourceCommands on every render. `publish()` (from commitDraft, or
|
|
17
|
-
// from the addCommands `post` above on undo/redo) fires SYNCHRONOUSLY inside the same call that
|
|
18
|
-
// just invoked `setSession`, i.e. before React has re-rendered — so a listener reading
|
|
19
|
-
// `editSession.session` directly would see the state from BEFORE this mutation. Deferring the
|
|
20
|
-
// read to a microtask (below) lets React's synchronous end-of-event-handler flush happen first,
|
|
21
|
-
// by which point this ref (updated every render, during the render itself) is current.
|
|
22
|
-
const sourceCommandsRef = useRef(editSession.session.sourceCommands);
|
|
23
|
-
sourceCommandsRef.current = editSession.session.sourceCommands;
|
|
24
|
-
// A publish can leave work queued behind React's render, while pdf.js can keep a loading task
|
|
25
|
-
// alive long after its caller has gone away. Keep hook lifetime separate from the shared preview
|
|
26
|
-
// sequence so document switches & unmounts invalidate both queued work & in-flight loads.
|
|
27
|
-
const mountedRef = useRef(false);
|
|
28
|
-
const lifecycleRef = useRef(0);
|
|
29
|
-
const queueEpochRef = useRef(0);
|
|
30
|
-
const publishEpochRef = useRef(0);
|
|
31
|
-
const pendingLoadingTaskRef = useRef(null);
|
|
32
|
-
const retirementBarrierRef = useRef(null);
|
|
33
|
-
const deferredResumeRef = useRef(null);
|
|
34
|
-
const resumeWatcherRef = useRef(null);
|
|
35
|
-
const handleRecomposePreviewRef = useRef(null);
|
|
36
|
-
const destroyedLoadingTasksRef = useRef(new WeakSet());
|
|
37
|
-
const taskDestroyPromisesRef = useRef(new WeakMap());
|
|
38
|
-
const taskOwnershipRef = useRef(new WeakMap());
|
|
39
|
-
const ensureDestroyLoadingTask = useCallback((task) => {
|
|
40
|
-
const existing = taskDestroyPromisesRef.current.get(task);
|
|
41
|
-
if (existing)
|
|
42
|
-
return existing;
|
|
43
|
-
const ownership = taskOwnershipRef.current.get(task);
|
|
44
|
-
const destroyPromise = (async () => {
|
|
45
|
-
try {
|
|
46
|
-
if (!task.destroyed)
|
|
47
|
-
await task.destroy();
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
// Destruction is best-effort during unmount/path replacement.
|
|
51
|
-
}
|
|
52
|
-
finally {
|
|
53
|
-
ownership?.document?.();
|
|
54
|
-
ownership?.worker();
|
|
55
|
-
destroyedLoadingTasksRef.current.add(task);
|
|
56
|
-
}
|
|
57
|
-
})();
|
|
58
|
-
taskDestroyPromisesRef.current.set(task, destroyPromise);
|
|
59
|
-
return destroyPromise;
|
|
60
|
-
}, []);
|
|
61
|
-
const waitForDestroy = useCallback(async (task) => {
|
|
62
|
-
if (!task)
|
|
63
|
-
return true;
|
|
64
|
-
const destroyPromise = ensureDestroyLoadingTask(task);
|
|
65
|
-
let timeout;
|
|
66
|
-
const completed = await Promise.race([
|
|
67
|
-
destroyPromise.then(() => true),
|
|
68
|
-
new Promise((resolve) => {
|
|
69
|
-
timeout = window.setTimeout(() => resolve(false), 500);
|
|
70
|
-
}),
|
|
71
|
-
]);
|
|
72
|
-
if (timeout !== undefined)
|
|
73
|
-
window.clearTimeout(timeout);
|
|
74
|
-
return completed;
|
|
75
|
-
}, [ensureDestroyLoadingTask]);
|
|
76
|
-
const beginRetirement = useCallback((task) => {
|
|
77
|
-
const promise = ensureDestroyLoadingTask(task);
|
|
78
|
-
retirementBarrierRef.current = { task, promise };
|
|
79
|
-
void promise.then(() => {
|
|
80
|
-
if (retirementBarrierRef.current?.task === task)
|
|
81
|
-
retirementBarrierRef.current = null;
|
|
82
|
-
});
|
|
83
|
-
return promise;
|
|
84
|
-
}, [ensureDestroyLoadingTask]);
|
|
85
|
-
const deferUntilRetirement = useCallback((lifecycle, publishEpoch, queueEpoch) => {
|
|
86
|
-
const barrier = retirementBarrierRef.current;
|
|
87
|
-
if (!barrier)
|
|
88
|
-
return;
|
|
89
|
-
deferredResumeRef.current = { lifecycle, publishEpoch, queueEpoch };
|
|
90
|
-
if (resumeWatcherRef.current === barrier.promise)
|
|
91
|
-
return;
|
|
92
|
-
const watcher = barrier.promise.then(() => {
|
|
93
|
-
if (resumeWatcherRef.current === watcher)
|
|
94
|
-
resumeWatcherRef.current = null;
|
|
95
|
-
const deferred = deferredResumeRef.current;
|
|
96
|
-
deferredResumeRef.current = null;
|
|
97
|
-
if (deferred &&
|
|
98
|
-
mountedRef.current &&
|
|
99
|
-
lifecycleRef.current === deferred.lifecycle &&
|
|
100
|
-
publishEpochRef.current === deferred.publishEpoch &&
|
|
101
|
-
queueEpochRef.current === deferred.queueEpoch)
|
|
102
|
-
void handleRecomposePreviewRef.current?.(deferred.lifecycle, deferred.publishEpoch);
|
|
103
|
-
});
|
|
104
|
-
resumeWatcherRef.current = watcher;
|
|
105
|
-
}, []);
|
|
106
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: refs arrive as stable hook params
|
|
107
|
-
const clearPreview = useCallback(() => {
|
|
108
|
-
previewSeqRef.current += 1;
|
|
109
|
-
const pending = pendingLoadingTaskRef.current;
|
|
110
|
-
pendingLoadingTaskRef.current = null;
|
|
111
|
-
const published = previewLoadingTaskRef.current;
|
|
112
|
-
previewLoadingTaskRef.current = null;
|
|
113
|
-
previewDocRef.current = null;
|
|
114
|
-
previewPagesRef.current = new Set();
|
|
115
|
-
if (pending) {
|
|
116
|
-
const pendingRetirement = beginRetirement(pending);
|
|
117
|
-
if (published && published !== pending)
|
|
118
|
-
void pendingRetirement.then(() => beginRetirement(published));
|
|
119
|
-
}
|
|
120
|
-
else if (published)
|
|
121
|
-
beginRetirement(published);
|
|
122
|
-
}, [beginRetirement]);
|
|
123
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: refs arrive as stable hook params
|
|
124
|
-
const handleRecomposePreview = useCallback(async (lifecycle, publishEpoch = publishEpochRef.current) => {
|
|
125
|
-
const isCurrent = () => mountedRef.current &&
|
|
126
|
-
lifecycleRef.current === lifecycle &&
|
|
127
|
-
publishEpochRef.current === publishEpoch;
|
|
128
|
-
if (!isCurrent())
|
|
129
|
-
return;
|
|
130
|
-
const sha = sourceShaRef.current;
|
|
131
|
-
if (!sha)
|
|
132
|
-
return; // nothing inspected yet — no possible edits either
|
|
133
|
-
const seq = ++previewSeqRef.current;
|
|
134
|
-
const queueEpoch = queueEpochRef.current;
|
|
135
|
-
const barrier = retirementBarrierRef.current;
|
|
136
|
-
if (barrier) {
|
|
137
|
-
const reaped = await waitForDestroy(barrier.task);
|
|
138
|
-
if (!reaped) {
|
|
139
|
-
deferUntilRetirement(lifecycle, publishEpoch, queueEpoch);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const previousPending = pendingLoadingTaskRef.current;
|
|
144
|
-
pendingLoadingTaskRef.current = null;
|
|
145
|
-
if (previousPending) {
|
|
146
|
-
beginRetirement(previousPending);
|
|
147
|
-
const reaped = await waitForDestroy(previousPending);
|
|
148
|
-
if (!reaped) {
|
|
149
|
-
deferUntilRetirement(lifecycle, publishEpoch, queueEpoch);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
retirementBarrierRef.current = null;
|
|
153
|
-
if (!isCurrent() || previewSeqRef.current !== seq)
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
let task = null;
|
|
157
|
-
try {
|
|
158
|
-
const composed = await recomposePdfPreview({ composePdfPreview }, documentPath, sha, sourceCommandsRef.current);
|
|
159
|
-
if (!isCurrent() || previewSeqRef.current !== seq)
|
|
160
|
-
return; // superseded while composing
|
|
161
|
-
task = pdfjs.getDocument(pdfDocumentInit(composed.candidateBytes.slice(), { wasmUrl: pdfWasmUrl() }));
|
|
162
|
-
taskOwnershipRef.current.set(task, {
|
|
163
|
-
worker: acquireResource({
|
|
164
|
-
kind: "pdf-preview-worker",
|
|
165
|
-
documentPath,
|
|
166
|
-
format: "pdf",
|
|
167
|
-
}),
|
|
168
|
-
document: null,
|
|
169
|
-
});
|
|
170
|
-
pendingLoadingTaskRef.current = task;
|
|
171
|
-
const doc = await task.promise;
|
|
172
|
-
if (!isCurrent() || previewSeqRef.current !== seq) {
|
|
173
|
-
if (pendingLoadingTaskRef.current === task)
|
|
174
|
-
pendingLoadingTaskRef.current = null;
|
|
175
|
-
beginRetirement(task);
|
|
176
|
-
const reaped = await waitForDestroy(task); // superseded while loading — this call's own task
|
|
177
|
-
if (!reaped)
|
|
178
|
-
deferUntilRetirement(lifecycle, publishEpoch, queueEpoch);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (pendingLoadingTaskRef.current === task)
|
|
182
|
-
pendingLoadingTaskRef.current = null;
|
|
183
|
-
const ownership = taskOwnershipRef.current.get(task);
|
|
184
|
-
if (ownership && !ownership.document) {
|
|
185
|
-
ownership.document = acquireResource({
|
|
186
|
-
kind: "pdf-preview-document",
|
|
187
|
-
documentPath,
|
|
188
|
-
format: "pdf",
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
const previousPublished = previewLoadingTaskRef.current;
|
|
192
|
-
// An undo can remove the last command, making the new candidate's affected-pages list
|
|
193
|
-
// empty. Re-render pages from the previous candidate too, so those slots switch back to
|
|
194
|
-
// the on-disk document instead of retaining stale edited pixels.
|
|
195
|
-
const pagesToRender = new Set([...previewPagesRef.current, ...composed.affectedPages]);
|
|
196
|
-
if (!isCurrent() || previewSeqRef.current !== seq) {
|
|
197
|
-
beginRetirement(task);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
previewLoadingTaskRef.current = task;
|
|
201
|
-
previewDocRef.current = doc;
|
|
202
|
-
previewPagesRef.current = new Set(composed.affectedPages);
|
|
203
|
-
if (previousPublished && previousPublished !== task)
|
|
204
|
-
beginRetirement(previousPublished);
|
|
205
|
-
for (const n of pagesToRender) {
|
|
206
|
-
renderedRef.current.get(n)?.();
|
|
207
|
-
void renderPageRef.current(n);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
if (pendingLoadingTaskRef.current === task)
|
|
212
|
-
pendingLoadingTaskRef.current = null;
|
|
213
|
-
if (task)
|
|
214
|
-
beginRetirement(task);
|
|
215
|
-
if (isCurrent() && previewSeqRef.current === seq) {
|
|
216
|
-
logError("pdf-editor:preview");
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}, [beginRetirement, deferUntilRetirement, documentPath, waitForDestroy]);
|
|
220
|
-
handleRecomposePreviewRef.current = handleRecomposePreview;
|
|
221
|
-
useEffect(() => {
|
|
222
|
-
mountedRef.current = true;
|
|
223
|
-
const lifecycle = ++lifecycleRef.current;
|
|
224
|
-
const unsubscribe = onPublish(() => {
|
|
225
|
-
const queueEpoch = queueEpochRef.current;
|
|
226
|
-
const publishEpoch = ++publishEpochRef.current;
|
|
227
|
-
queueMicrotask(() => {
|
|
228
|
-
if (mountedRef.current &&
|
|
229
|
-
lifecycleRef.current === lifecycle &&
|
|
230
|
-
queueEpochRef.current === queueEpoch &&
|
|
231
|
-
publishEpochRef.current === publishEpoch) {
|
|
232
|
-
void handleRecomposePreview(lifecycle, publishEpoch);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
return () => {
|
|
237
|
-
unsubscribe();
|
|
238
|
-
mountedRef.current = false;
|
|
239
|
-
lifecycleRef.current += 1;
|
|
240
|
-
clearPreview();
|
|
241
|
-
};
|
|
242
|
-
}, [clearPreview, handleRecomposePreview, onPublish]);
|
|
243
|
-
// Save commits invalidate queued/in-flight candidate work without destroying the currently
|
|
244
|
-
// published preview. If reload fails, that preview remains visible as last-known-good evidence;
|
|
245
|
-
// a successful reload clears it after the fresh document is installed. Later source publishes
|
|
246
|
-
// remain valid because lifecycleRef is deliberately left untouched.
|
|
247
|
-
const invalidatePreview = useCallback(() => {
|
|
248
|
-
queueEpochRef.current += 1;
|
|
249
|
-
previewSeqRef.current += 1;
|
|
250
|
-
const pending = pendingLoadingTaskRef.current;
|
|
251
|
-
pendingLoadingTaskRef.current = null;
|
|
252
|
-
if (pending)
|
|
253
|
-
beginRetirement(pending);
|
|
254
|
-
}, [beginRetirement, previewSeqRef]);
|
|
255
|
-
return { invalidatePreview };
|
|
256
|
-
}
|