diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index e6e09540c..3271e670b 100644
--- a/docs/api/openapi.yaml
+++ b/docs/api/openapi.yaml
@@ -63012,6 +63012,23 @@ components:
type: array
items:
$ref: "#/components/schemas/OcrExtractedRow"
+ pageCoverage:
+ description: "Present only when a PDF ran past the pages a scan reads: the rows come from the first `read` of `total`
+ pages. Absent when the whole document was read."
+ type: object
+ properties:
+ read:
+ type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ total:
+ type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ required:
+ - read
+ - total
+ additionalProperties: false
required:
- reportDate
- providerType
diff --git a/messages/de.json b/messages/de.json
index 553d63b6c..6bf4db0c6 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -8992,6 +8992,7 @@
"reviewTitle": "Vor dem Speichern prüfen",
"reviewHint": "Prüfe jeden Wert und speichere die gewünschten Einträge.",
"foundCount": "{count} Werte gefunden",
+ "pagesCapped": "Nur die ersten {read} von {total} Seiten wurden gelesen. Werte auf späteren Seiten fehlen in dieser Liste.",
"confirmRow": "Diesen Wert übernehmen",
"analyteLabel": "Bezeichnung",
"resultLabel": "Ergebnis",
diff --git a/messages/en.json b/messages/en.json
index 55d0718d6..04555344a 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -8992,6 +8992,7 @@
"reviewTitle": "Review before saving",
"reviewHint": "Check each reading, then save the ones you want to keep.",
"foundCount": "{count} readings found",
+ "pagesCapped": "Only the first {read} of {total} pages were read. Values on later pages are not in this list.",
"confirmRow": "Include this reading",
"analyteLabel": "Test name",
"resultLabel": "Result",
diff --git a/messages/es.json b/messages/es.json
index 1718c1acc..c90525051 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -8992,6 +8992,7 @@
"reviewTitle": "Revisar antes de guardar",
"reviewHint": "Revisa cada valor y guarda los que quieras conservar.",
"foundCount": "{count} valores encontrados",
+ "pagesCapped": "Solo se leyeron las primeras {read} de {total} páginas. Los valores de las páginas siguientes no están en esta lista.",
"confirmRow": "Incluir este valor",
"analyteLabel": "Nombre de la prueba",
"resultLabel": "Resultado",
diff --git a/messages/fr.json b/messages/fr.json
index 0afad1da7..1ebea9c37 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -8992,6 +8992,7 @@
"reviewTitle": "Vérifier avant d'enregistrer",
"reviewHint": "Vérifiez chaque valeur, puis enregistrez celles à conserver.",
"foundCount": "{count} valeurs trouvées",
+ "pagesCapped": "Seules les {read} premières pages sur {total} ont été lues. Les valeurs des pages suivantes ne figurent pas dans cette liste.",
"confirmRow": "Inclure cette valeur",
"analyteLabel": "Nom de l'analyse",
"resultLabel": "Résultat",
diff --git a/messages/it.json b/messages/it.json
index 883d3c4a1..c163f1bef 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -8992,6 +8992,7 @@
"reviewTitle": "Controlla prima di salvare",
"reviewHint": "Controlla ogni valore, poi salva quelli da conservare.",
"foundCount": "{count} valori trovati",
+ "pagesCapped": "Sono state lette solo le prime {read} pagine su {total}. I valori delle pagine successive non sono in questo elenco.",
"confirmRow": "Includi questo valore",
"analyteLabel": "Nome dell'esame",
"resultLabel": "Risultato",
diff --git a/messages/ko.json b/messages/ko.json
index f814ab916..08156e094 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -8992,6 +8992,7 @@
"reviewTitle": "저장 전에 확인하기",
"reviewHint": "측정값을 하나씩 확인하고, 남길 항목만 저장하세요.",
"foundCount": "측정값 {count}개를 찾았어요",
+ "pagesCapped": "전체 {total}쪽 중 처음 {read}쪽만 읽었습니다. 이후 쪽의 값은 이 목록에 없습니다.",
"confirmRow": "이 측정값 포함",
"analyteLabel": "검사 항목",
"resultLabel": "결과",
diff --git a/messages/pl.json b/messages/pl.json
index 7e45f4587..c0ecb344a 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -8992,6 +8992,7 @@
"reviewTitle": "Sprawdź przed zapisaniem",
"reviewHint": "Sprawdź każdy wynik, a następnie zapisz te, które chcesz zachować.",
"foundCount": "Znaleziono {count} wyników",
+ "pagesCapped": "Odczytano tylko pierwsze {read} z {total} stron. Wartości z dalszych stron nie ma na tej liście.",
"confirmRow": "Uwzględnij ten wynik",
"analyteLabel": "Nazwa badania",
"resultLabel": "Wynik",
diff --git a/src/app/api/labs/ocr/extract/__tests__/route.test.ts b/src/app/api/labs/ocr/extract/__tests__/route.test.ts
index 4dcd20385..742700fb7 100644
--- a/src/app/api/labs/ocr/extract/__tests__/route.test.ts
+++ b/src/app/api/labs/ocr/extract/__tests__/route.test.ts
@@ -229,11 +229,15 @@ describe("POST /api/labs/ocr/extract — vision PDF rasterization", () => {
vi.mocked(rasterizePdf).mockResolvedValue({
ok: true,
images: [{ mediaType: "image/jpeg", dataBase64: "cGFnZQ==" }],
+ pageCount: 1,
});
vi.mocked(runOcrExtraction).mockResolvedValue({ rows: [] } as never);
const res = await POST(pdfReq());
expect(res.status).toBe(200);
+ // The whole PDF was read: no coverage note on the response.
+ const whole = (await res.json()) as { data: Record
); } + +/** + * The server read only the first pages of a long PDF (`pageCoverage` on the + * extract response). Said on the review screen, beside the rows, because the + * person checking the list is the one who would otherwise assume the whole + * report was read. Renders nothing when the whole document was read. + */ +export function OcrPageCoverageNote({ + coverage, +}: { + coverage: { read: number; total: number } | null; +}) { + const { t } = useTranslations(); + if (!coverage) return null; + return ( +
+ {t("labs.ocr.pagesCapped", { + read: coverage.read, + total: coverage.total, + })} +
+ ); +} diff --git a/src/lib/documents/__tests__/rasterize-pdf.test.ts b/src/lib/documents/__tests__/rasterize-pdf.test.ts index a00340e8a..cb124491b 100644 --- a/src/lib/documents/__tests__/rasterize-pdf.test.ts +++ b/src/lib/documents/__tests__/rasterize-pdf.test.ts @@ -90,6 +90,9 @@ describe("rasterizePdf", () => { expect(result.ok).toBe(true); if (!result.ok) return; expect(result.images).toHaveLength(RASTER_MAX_PAGES); + // The page count travels with the result, so a caller can say the + // reading stopped short. + expect(result.pageCount).toBe(RASTER_MAX_PAGES + 4); }); it("honours a caller maxPages bound (thumbnail path renders page 1 only)", async () => { diff --git a/src/lib/documents/rasterize-pdf.ts b/src/lib/documents/rasterize-pdf.ts index 402f61695..f61e74fc3 100644 --- a/src/lib/documents/rasterize-pdf.ts +++ b/src/lib/documents/rasterize-pdf.ts @@ -74,7 +74,16 @@ export interface RasterImage { * would not render" (`render-failed` — malformed / encrypted / zero pages). */ export type RasterResult = - | { ok: true; images: RasterImage[] } + | { + ok: true; + images: RasterImage[]; + /** + * Pages in the PDF. Above `images.length` when the page cap cut the + * render short, which a caller showing the result has to say: the + * reading covers the first pages only. + */ + pageCount: number; + } | { ok: false; reason: "unsupported" | "render-failed" }; // Minimal structural types for the slice of the pdfjs API this module uses. A @@ -212,7 +221,7 @@ export async function rasterizePdf( action: { name: "documents.rasterize.ok" }, meta: { pages: images.length, cappedFrom: doc.numPages }, }); - return { ok: true, images }; + return { ok: true, images, pageCount: doc.numPages }; } catch (err) { annotate({ action: { name: "documents.rasterize.failed" }, diff --git a/src/lib/openapi/routes/ocr.ts b/src/lib/openapi/routes/ocr.ts index 82ded9d09..f894cf8aa 100644 --- a/src/lib/openapi/routes/ocr.ts +++ b/src/lib/openapi/routes/ocr.ts @@ -85,6 +85,15 @@ const extractResponse = z reportDate: z.string().nullable(), providerType: z.string(), rows: z.array(extractedRow), + pageCoverage: z + .object({ + read: z.number().int().positive(), + total: z.number().int().positive(), + }) + .optional() + .describe( + "Present only when a PDF ran past the pages a scan reads: the rows come from the first `read` of `total` pages. Absent when the whole document was read.", + ), }) .meta({ id: "OcrExtractResponse", diff --git a/src/lib/validations/labs-ocr.ts b/src/lib/validations/labs-ocr.ts index aa18dd7e6..000a78ab5 100644 --- a/src/lib/validations/labs-ocr.ts +++ b/src/lib/validations/labs-ocr.ts @@ -132,6 +132,12 @@ export interface OcrExtractResponseDto { reportDate: string | null; providerType: string; rows: OcrExtractedRowDto[]; + /** + * Present only when a PDF was longer than the pages the scan reads + * (`RASTER_MAX_PAGES`): the rows come from the first `read` of `total` + * pages, and the review screen says so. + */ + pageCoverage?: { read: number; total: number }; } /**