Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`

Expand Down
18 changes: 2 additions & 16 deletions public/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -577,24 +577,10 @@ <h2 class="visually-hidden">About</h2>
$('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));
Expand Down
8 changes: 1 addition & 7 deletions src/routes/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -665,12 +660,11 @@ export function sessionsRouter(cfg: IrisConfig, store: Store): Router {
pdfPath: src.pdf,
input,
values: values as Record<string, unknown>,
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
Expand Down
4 changes: 1 addition & 3 deletions src/util/taggedPdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>; retag?: boolean; scratchRoot: string; timeoutSeconds: number },
args: { pdfPath: string; input: TagInput; values: Record<string, unknown>; scratchRoot: string; timeoutSeconds: number },
): Promise<{ pdf: Buffer; report: unknown }> {
mkdirSync(args.scratchRoot, { recursive: true });
const dir = mkdtempSync(join(args.scratchRoot, "pdf-"));
Expand All @@ -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) {
Expand Down
39 changes: 10 additions & 29 deletions test/demo-pdf-form.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] }), []);
Expand Down Expand Up @@ -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."]);
});
5 changes: 2 additions & 3 deletions test/fixtures/fake-iris-pdf.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand All @@ -31,15 +31,14 @@ 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);
if ("slow" in values) await new Promise((r) => setTimeout(r, 5000));
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);
}
16 changes: 0 additions & 16 deletions test/tagged-pdf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading