diff --git a/docs/API.md b/docs/API.md index 44793dd8..1fc61eaa 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1005,10 +1005,10 @@ only which field names were given. The answer is `{"filename", "pdf", "report"}`: the file name to save as, the PDF in base64, and the tagger's report. `report.warnings` lists what the tagger could not do cleanly, such as a form field -it could not find in the HTML. The demo page turns each warning into a plain sentence. - -A PDF that is already tagged gets `422 already_tagged`. Send `"retag": true` to replace its tags -with tags from the HTML. The report then has a `retagged` warning. +it could not find in the HTML. The demo page turns each warning into a plain sentence. A PDF that +was already tagged has its old tags replaced, with a `retagged` warning. This needs iris-pdf with +[equalify-iris-pdf#8](https://github.com/EqualifyEverything/equalify-iris-pdf/pull/8). An older one +answers `422 already_tagged`. The session must have finished (`ready_for_review` or `closed`), or you get `409 invalid_state`. The PDF is tagged from the first pass's HTML for each page, before review. A page that failed extraction is @@ -4368,10 +4368,9 @@ pages reported blank, which an empty document could not. ### `tagged_pdf` / `tagged_pdf_failed` A [tagged PDF](#get-a-tagged-pdf-optional) was made, or the tagger refused. `ms` is how long it took. -`tagged_pdf` lists `fields_given`, the names of the fields that were filled in, and `retag`. The -values are never logged. `tagged_pdf_failed` has the tagger's `code` and `error`. An `internal_error`'s -message is left out, because it may quote a value. The demo asks before it retags, so a demo retag -logs `tagged_pdf_failed` with `already_tagged` first, then `tagged_pdf` with `retag: true`. +`tagged_pdf` lists `fields_given`, the names of the fields that were filled in. The values are +never logged. `tagged_pdf_failed` has the tagger's `code` and `error`. An `internal_error`'s +message is left out, because it may quote a value. ### `form_fields` / `page_fields` / `page_fields_missing` diff --git a/public/demo.html b/public/demo.html index d200ae16..441822cd 100644 --- a/public/demo.html +++ b/public/demo.html @@ -577,24 +577,10 @@

About

$('pdf-btn').disabled = true; live('Tagging your PDF. This can take a minute.'); try { - const values = fieldValues($('pdf-field-list')); - const post = (retag) => api('/sessions/' + sessionId + '/pdf', { + const res = await api('/sessions/' + sessionId + '/pdf', { method: 'POST', headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ values, retag }), + body: JSON.stringify({ values: fieldValues($('pdf-field-list')) }), }); - let res = await post(false); - // A PDF that is already tagged is only retagged if the reader says so. - if (res.status === 422) { - const code = await res.clone().json().then((j) => j.error && j.error.code, () => null); - if (code === 'already_tagged') { - if (!confirm('This PDF is already tagged. Retagging replaces its existing tags with tags from Iris\'s HTML. Retag it?')) { - live('The PDF was not retagged.'); - return; - } - live('Retagging your PDF.'); - res = await post(true); - } - } if (!res.ok) throw new Error(await errMessage(res)); const out = await res.json(); const bytes = Uint8Array.from(atob(out.pdf), (c) => c.charCodeAt(0)); diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts index 2996133c..527db36f 100644 --- a/src/routes/sessions.ts +++ b/src/routes/sessions.ts @@ -634,11 +634,6 @@ export function sessionsRouter(cfg: IrisConfig, store: Store): Router { sendError(res, 400, "invalid_request", "`values` must be an object of field name to value."); return; } - const retag: unknown = (req.body ?? {}).retag ?? false; - if (typeof retag !== "boolean") { - sendError(res, 400, "invalid_request", "`retag` must be true or false."); - return; - } if (tooBusy(res)) return; const log = new RunLog(paths.sessionLog(s.session_id)); const started = Date.now(); @@ -665,12 +660,11 @@ export function sessionsRouter(cfg: IrisConfig, store: Store): Router { pdfPath: src.pdf, input, values: values as Record, - retag, scratchRoot: paths.pdfScratchRoot(), timeoutSeconds: tagTimeoutSeconds(cfg), }); // Field names only. A value is never logged. - log.event("tagged_pdf", { ms: Date.now() - started, fields_given: Object.keys(values), retag }); + log.event("tagged_pdf", { ms: Date.now() - started, fields_given: Object.keys(values) }); res.json({ filename: `${base}_tagged.pdf`, pdf: pdf.toString("base64"), report }); } catch (e) { // iris-pdf's own messages name fields and options, never values. Its diff --git a/src/util/taggedPdf.ts b/src/util/taggedPdf.ts index 76138089..c17c8a96 100644 --- a/src/util/taggedPdf.ts +++ b/src/util/taggedPdf.ts @@ -121,7 +121,7 @@ export type TagInput = { lang?: string; title?: string; pages: { sourcePage: num // neither them nor the tagged PDF. export async function tagPdf( command: string, - args: { pdfPath: string; input: TagInput; values: Record; retag?: boolean; scratchRoot: string; timeoutSeconds: number }, + args: { pdfPath: string; input: TagInput; values: Record; scratchRoot: string; timeoutSeconds: number }, ): Promise<{ pdf: Buffer; report: unknown }> { mkdirSync(args.scratchRoot, { recursive: true }); const dir = mkdtempSync(join(args.scratchRoot, "pdf-")); @@ -130,8 +130,6 @@ export async function tagPdf( writeFileSync(f("pages.json"), JSON.stringify(args.input)); writeFileSync(f("values.json"), JSON.stringify(args.values), { mode: 0o600 }); const argv = ["tag", "--pdf", args.pdfPath, "--pages", f("pages.json"), "--values", f("values.json"), "--out", f("out.pdf"), "--report", f("report.json")]; - // An already-tagged PDF is refused (`already_tagged`) unless the user asked to replace its tags. - if (args.retag) argv.push("--retag"); try { await execFileP(command, argv, { maxBuffer: MAX_BUFFER, timeout: args.timeoutSeconds * 1000 }); } catch (e) { diff --git a/test/demo-pdf-form.test.ts b/test/demo-pdf-form.test.ts index 4d4c1e31..5153d005 100644 --- a/test/demo-pdf-form.test.ts +++ b/test/demo-pdf-form.test.ts @@ -95,12 +95,14 @@ test("the report's warnings come back as one sentence per kind", () => { { code: "missing_alt", page: 2 }, { code: "field_not_in_html", page: 1 }, { code: "no_title" }, + { code: "retagged" }, { code: "something_new" }, ], }); assert.deepEqual(notes, [ "2 images have no description.", "One form field was not found in the HTML, so a screen reader reaches it at the end of its page.", + "The PDF's old tags were replaced with tags from the HTML.", "Note from the tagger: something_new.", ]); assert.deepEqual(pdfNotes({ warnings: [] }), []); @@ -130,40 +132,19 @@ test("a late /fields reply is dropped after a new document, a re-run, or a newer }); // The submit handler, run against stand-ins for the page and the server. -function submitHandler(replies: { status: number; body: unknown }[], answer: boolean) { +test("the demo sends only the values, and shows a refusal as an error", async () => { const start = demoHtml.indexOf("async (ev) =>", demoHtml.indexOf("$('pdf-form').addEventListener('submit'")); const end = demoHtml.lastIndexOf("});", demoHtml.indexOf("$('restart-btn')")); - const sent: unknown[] = [], said: string[] = [], asked: string[] = [], errors: string[] = []; + const sent: unknown[] = [], errors: string[] = []; const api = async (_path: string, opts: { body: string }) => { sent.push(JSON.parse(opts.body)); - const r = replies.shift()!; - const res = { ok: r.status < 400, status: r.status, json: async () => r.body, clone: () => res }; - return res; + return { ok: false, status: 422, json: async () => ({ error: { code: "encrypted", message: "The PDF is encrypted." } }) }; }; - const $ = () => ({ disabled: false }); const handler = new Function( - "$", "sessionId", "api", "confirm", "live", "setError", "fieldValues", "errMessage", + "$", "sessionId", "api", "live", "setError", "fieldValues", "errMessage", `return ${demoHtml.slice(start, end)}};`, - )($, "ses_1", api, (q: string) => (asked.push(q), answer), (m: string) => said.push(m), (m: string) => m && errors.push(m), () => ({}), async (res: { json: () => Promise<{ error: { message: string } }> }) => (await res.json()).error.message); - return { run: () => handler({ preventDefault() {} }), sent, said, asked, errors }; -} - -test("an already-tagged PDF is retagged only after the reader says yes", async () => { - const already = { status: 422, body: { error: { code: "already_tagged", message: "The PDF is already tagged." } } }; - const no = submitHandler([already], false); - await no.run(); - assert.deepEqual(no.sent, [{ values: {}, retag: false }]); - assert.equal(no.asked.length, 1); - assert.deepEqual(no.errors, []); - assert.equal(no.said.at(-1), "The PDF was not retagged."); - - const yes = submitHandler([already, { status: 500, body: { error: { message: "stop here" } } }], true); - await yes.run(); - assert.deepEqual(yes.sent, [{ values: {}, retag: false }, { values: {}, retag: true }]); - assert.deepEqual(yes.errors, ["Could not tag the PDF: stop here"]); - - // Any other refusal is shown, not asked about. - const other = submitHandler([{ status: 422, body: { error: { code: "encrypted", message: "The PDF is encrypted." } } }], true); - await other.run(); - assert.deepEqual([other.asked, other.sent.length, other.errors], [[], 1, ["Could not tag the PDF: The PDF is encrypted."]]); + )(() => ({ disabled: false }), "ses_1", api, () => {}, (m: string) => m && errors.push(m), () => ({ name: "Ada" }), async (res: { json: () => Promise<{ error: { message: string } }> }) => (await res.json()).error.message); + await handler({ preventDefault() {} }); + assert.deepEqual(sent, [{ values: { name: "Ada" } }]); + assert.deepEqual(errors, ["Could not tag the PDF: The PDF is encrypted."]); }); diff --git a/test/fixtures/fake-iris-pdf.mjs b/test/fixtures/fake-iris-pdf.mjs index 50a06142..bc02045b 100755 --- a/test/fixtures/fake-iris-pdf.mjs +++ b/test/fixtures/fake-iris-pdf.mjs @@ -8,7 +8,7 @@ const [command, ...rest] = process.argv.slice(2); const { values: a } = parseArgs({ args: rest, strict: false, - options: { pdf: { type: "string" }, pages: { type: "string" }, values: { type: "string" }, out: { type: "string" }, report: { type: "string" }, json: { type: "boolean" }, retag: { type: "boolean" }, help: { type: "boolean" } }, + options: { pdf: { type: "string" }, pages: { type: "string" }, values: { type: "string" }, out: { type: "string" }, report: { type: "string" }, json: { type: "boolean" }, help: { type: "boolean" } }, }); const fail = (code, message, exit) => { process.stderr.write(`iris-pdf: ${code}: ${message}\n`); @@ -31,7 +31,6 @@ if (command === "fields") { : []), ])); } else if (command === "tag") { - if (pdf.includes("TAGGED") && !a.retag) fail("already_tagged", "The PDF is already tagged. Pass --retag to replace its tags.", 1); const values = JSON.parse(readFileSync(a.values, "utf8")); if ("unknown.field" in values) fail("bad_value", "No field is named unknown.field.", 3); if ("crash" in values) fail("internal_error", `Cannot set ${values.crash}.`, 3); @@ -39,7 +38,7 @@ if (command === "fields") { const input = JSON.parse(readFileSync(a.pages, "utf8")); const mode = (statSync(a.values).mode & 0o777).toString(8); writeFileSync(a.out, "%PDF-fake " + JSON.stringify({ source: pdf, input, values, mode, scratch: a.values })); - writeFileSync(a.report, JSON.stringify({ tool: "iris-pdf 0.0.0-fake", warnings: [{ code: "duplicate_text_layer", page: 1 }, ...(a.retag ? [{ code: "retagged" }] : [])] })); + writeFileSync(a.report, JSON.stringify({ tool: "iris-pdf 0.0.0-fake", warnings: [{ code: "duplicate_text_layer", page: 1 }] })); } else { fail("bad_arguments", `Unknown command ${command}.`, 3); } diff --git a/test/tagged-pdf.test.ts b/test/tagged-pdf.test.ts index 5607630a..df884f43 100644 --- a/test/tagged-pdf.test.ts +++ b/test/tagged-pdf.test.ts @@ -248,22 +248,6 @@ test("POST /pdf passes the tagger's refusals on with its code", async () => { } }); -test("POST /pdf refuses an already-tagged PDF unless asked to retag it", async () => { - const s = await serve({ source: "%PDF TAGGED" }); - try { - const refused = await s.tag({ values: {} }); - assert.equal(refused.status, 422); - assert.equal((await refused.json()).error.code, "already_tagged"); - assert.equal((await s.tag({ retag: "yes" })).status, 400); - const res = await s.tag({ values: {}, retag: true }); - assert.equal(res.status, 200); - assert.deepEqual((await res.json()).report.warnings.map((w: { code: string }) => w.code), ["duplicate_text_layer", "retagged"]); - assert.match(readFileSync(s.paths.sessionLog(s.id), "utf8"), /"tagged_pdf".*"retag":true/); - } finally { - s.close(); - } -}); - test("POST /pdf waits for the finished document and stops a run that takes too long", async () => { const running = await serve({ status: "running" }); try {