smart redaction (#6195)

Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
EthanHealy01
2026-06-03 16:16:33 +00:00
committed by GitHub
co-authored by James Brunton
parent 7f3ca7ea70
commit 35a712a278
26 changed files with 4397 additions and 1802 deletions
@@ -64,15 +64,16 @@ async function extractPageMeasureScales(
// Parse a Measure dict into a MeasureScale, or return null if malformed.
const parseScale = (measureObj: unknown) => {
if (!(measureObj instanceof PDFDict)) return null;
const rObj = measureObj.lookup(PDFName.of("R"));
// @cantoo/pdf-lib ships without individual .d.ts files so instanceof can't narrow `unknown`
const m = measureObj as PDFDict;
const rObj = m.lookup(PDFName.of("R"));
const ratioLabel =
rObj instanceof PDFString || rObj instanceof PDFHexString
? rObj.decodeText()
: "";
// D = distance array, X = x-axis fallback
let fmtArray = measureObj.lookup(PDFName.of("D"));
if (!(fmtArray instanceof PDFArray))
fmtArray = measureObj.lookup(PDFName.of("X"));
let fmtArray = m.lookup(PDFName.of("D"));
if (!(fmtArray instanceof PDFArray)) fmtArray = m.lookup(PDFName.of("X"));
if (!(fmtArray instanceof PDFArray)) return null;
const firstFmt = fmtArray.lookup(0);
if (!(firstFmt instanceof PDFDict)) return null;
@@ -22,6 +22,20 @@ import type {
ButtonAction,
} from "@app/tools/formFill/types";
import type { IFormDataProvider } from "@app/tools/formFill/providers/types";
import type {
PDFDict,
PDFString,
PDFHexString,
PDFName,
} from "@cantoo/pdf-lib";
interface PDFAcroField {
dict: PDFDict;
getWidgets?: () => Array<{ dict: PDFDict }>;
}
interface PDFFieldInternal {
acroField?: PDFAcroField;
}
import {
closeDocAndFreeBuffer,
extractFormFields,
@@ -238,7 +252,11 @@ export class PdfiumFormProvider implements IFormDataProvider {
);
const altName = readUtf16(m, altBuf, altLen);
m.pdfium.wasmExports.free(altBuf);
(nameToField.get(name) as any)._tooltip = altName || null;
(
nameToField.get(name) as PdfiumFormField & {
_tooltip?: string | null;
}
)._tooltip = altName || null;
}
enriched.add(name);
}
@@ -299,7 +317,7 @@ export class PdfiumFormProvider implements IFormDataProvider {
const decodeText = (obj: unknown): string => {
if (obj instanceof PDFString || obj instanceof PDFHexString)
return obj.decodeText();
return (obj as PDFString | PDFHexString).decodeText();
return String(obj ?? "");
};
@@ -312,7 +330,8 @@ export class PdfiumFormProvider implements IFormDataProvider {
)
continue;
const acroDict = (field.acroField as any).dict;
const acroDict = (field as unknown as PDFFieldInternal).acroField!
.dict;
const optRaw = acroDict.lookup(PDFName.of("Opt"));
if (!(optRaw instanceof PDFArray)) continue;
@@ -374,8 +393,14 @@ export class PdfiumFormProvider implements IFormDataProvider {
if (buttons.length === 0) return result;
try {
const { PDFDocument, PDFName, PDFString, PDFHexString, PDFDict } =
await import("@cantoo/pdf-lib");
const {
PDFDocument,
PDFName,
PDFString,
PDFHexString,
PDFDict,
PDFNumber,
} = await import("@cantoo/pdf-lib");
const doc = await PDFDocument.load(data, {
ignoreEncryption: true,
@@ -385,53 +410,53 @@ export class PdfiumFormProvider implements IFormDataProvider {
const decodeText = (obj: unknown): string | null => {
if (obj instanceof PDFString || obj instanceof PDFHexString)
return obj.decodeText();
return (obj as PDFString | PDFHexString).decodeText();
if (obj instanceof PDFName)
return (obj as any).asString?.() ?? obj.toString().replace(/^\//, "");
return (obj as PDFName).asString() ?? String(obj).replace(/^\//, "");
return null;
};
const parseActionDict = (aObj: unknown): ButtonAction | null => {
if (!(aObj instanceof PDFDict)) return null;
const sObj = aObj.lookup(PDFName.of("S"));
// @cantoo/pdf-lib ships without individual .d.ts files so instanceof can't narrow `unknown`
const a = aObj as PDFDict;
const sObj = a.lookup(PDFName.of("S"));
if (!(sObj instanceof PDFName)) return null;
const actionType: string =
(sObj as any).asString?.() ?? sObj.toString().replace(/^\//, "");
(sObj as PDFName).asString() ?? String(sObj).replace(/^\//, "");
switch (actionType) {
case "Named": {
const nObj = aObj.lookup(PDFName.of("N"));
const nObj = a.lookup(PDFName.of("N"));
const name =
nObj instanceof PDFName
? ((nObj as any).asString?.() ??
nObj.toString().replace(/^\//, ""))
? ((nObj as PDFName).asString() ??
String(nObj).replace(/^\//, ""))
: "";
return { type: "named", namedAction: name };
}
case "JavaScript": {
const jsObj = aObj.lookup(PDFName.of("JS"));
const jsObj = a.lookup(PDFName.of("JS"));
const js = decodeText(jsObj) ?? jsObj?.toString() ?? "";
return { type: "javascript", javascript: js };
}
case "SubmitForm": {
const fObj = aObj.lookup(PDFName.of("F"));
const fObj = a.lookup(PDFName.of("F"));
let url = "";
if (fObj instanceof PDFDict) {
url = decodeText(fObj.lookup(PDFName.of("F"))) ?? "";
} else if (fObj) {
url = decodeText(fObj) ?? fObj.toString();
url = decodeText(fObj) ?? String(fObj);
}
const flagsObj = aObj.lookup(PDFName.of("Flags"));
const flagsObj = a.lookup(PDFName.of("Flags"));
const flags =
typeof (flagsObj as any)?.asNumber === "function"
? (flagsObj as any).asNumber()
: 0;
flagsObj instanceof PDFNumber ? flagsObj.asNumber() : 0;
return { type: "submitForm", url, submitFlags: flags };
}
case "ResetForm":
return { type: "resetForm" };
case "URI": {
const uriObj = aObj.lookup(PDFName.of("URI"));
const uriObj = a.lookup(PDFName.of("URI"));
return { type: "uri", url: decodeText(uriObj) ?? "" };
}
default:
@@ -439,7 +464,7 @@ export class PdfiumFormProvider implements IFormDataProvider {
}
};
const getMkCaption = (dict: any): string | null => {
const getMkCaption = (dict: PDFDict): string | null => {
try {
const mkObj = dict.lookup(PDFName.of("MK"));
if (!(mkObj instanceof PDFDict)) return null;
@@ -450,7 +475,7 @@ export class PdfiumFormProvider implements IFormDataProvider {
}
};
const getActionFromDict = (dict: any): ButtonAction | null => {
const getActionFromDict = (dict: PDFDict): ButtonAction | null => {
try {
return parseActionDict(dict.lookup(PDFName.of("A")));
} catch {
@@ -465,13 +490,13 @@ export class PdfiumFormProvider implements IFormDataProvider {
if (!buttonNames.has(name)) continue;
try {
const acroField = (field as any).acroField;
const acroField = (field as unknown as PDFFieldInternal).acroField;
if (!acroField?.dict) continue;
const info: { label?: string; action?: ButtonAction } = {};
// Try widget dicts first (each widget can have its own /MK and /A)
const widgets: any[] = (acroField as any).getWidgets?.() ?? [];
const widgets = acroField.getWidgets?.() ?? [];
for (const widget of widgets) {
if (!info.label) {
const label = getMkCaption(widget.dict);
@@ -25,7 +25,13 @@ export function usePrototypeToolRegistry(): PrototypeToolRegistry {
() =>
({
pdfCommentAgent: {
icon: <LocalIcon icon="add-comment" width="1.5rem" height="1.5rem" />,
icon: (
<LocalIcon
icon="add-comment-outline-rounded"
width="1.5rem"
height="1.5rem"
/>
),
name: t("home.pdfCommentAgent.title", "Add AI comments"),
component: PdfCommentAgent,
description: t(