import axios from "axios";
type FieldValue = string | boolean | null;
type FieldKind = "text" | "checkbox" | "other";
function trimStr(v: unknown): string {
return String(v ?? "").trim();
}
/** True if the field has a value that counts as “filled” for PDF-required validation. */
function isFilled(value: FieldValue, kind: FieldKind): boolean {
if (kind === "checkbox") return value === true || value === false;
if (kind === "text") {
return typeof value === "string" && value.trim().length > 0;
}
return value != null && String(value).trim().length > 0;
}
type WidgetAnnotation = {
fieldName?: string;
fieldValue?: string | null;
fieldType?: string;
checkBox?: boolean;
required?: boolean;
fieldFlags?: number;
};
const FIELD_FLAG_REQUIRED = 2;
function isAnnotationRequired(a: WidgetAnnotation): boolean {
if (typeof a.required === "boolean") return a.required;
const ff = a.fieldFlags;
return typeof ff === "number" && (ff & FIELD_FLAG_REQUIRED) !== 0;
}
function kindFromAnnotation(a: WidgetAnnotation): FieldKind {
const ft = a.fieldType;
if (ft === "Tx") return "text";
if (ft === "Btn") {
if (a.checkBox) return "checkbox";
return "other";
}
if (ft === "Ch") return "other";
return "other";
}
function valueFromAnnotation(a: WidgetAnnotation): FieldValue {
const ft = a.fieldType;
const raw = a.fieldValue;
if (ft === "Tx") return trimStr(raw ?? "");
if (ft === "Btn" && a.checkBox) {
const v = raw == null ? "" : String(raw);
if (v === "" || v === "Off") return false;
return true;
}
if (ft === "Ch") return trimStr(raw ?? "");
return null;
}
/** pdf.js expects browser globals; Windmill workers do not provide `DOMMatrix`. */
async function ensureDomMatrixPolyfill(): Promise<void> {
if (typeof globalThis.DOMMatrix !== "undefined") return;
const mod = await import("dommatrix");
const DM = (mod as { default?: typeof globalThis.DOMMatrix }).default ?? (mod as { DOMMatrix: typeof globalThis.DOMMatrix }).DOMMatrix;
if (typeof DM === "function") {
Object.defineProperty(globalThis, "DOMMatrix", { value: DM, configurable: true });
}
}
async function loadFormData(
pdfBytes: Uint8Array,
password: string,
): Promise<{
values: Record<string, FieldValue>;
kinds: Record<string, FieldKind>;
available_fields: string[];
required_fields: string[];
}> {
await ensureDomMatrixPolyfill();
const { getDocument } = await import("pdfjs-dist/legacy/build/pdf.mjs");
const loadingTask = getDocument({
data: pdfBytes,
password,
disableRange: true,
disableStream: true,
useSystemFonts: true,
});
let pdf: { numPages: number; getPage: (n: number) => Promise<{ getAnnotations: () => Promise<unknown[]> }> };
try {
pdf = await loadingTask.promise;
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (/password/i.test(msg)) {
throw new Error(
"PDF needs a password or a different one. Set `pdfPassword` or use an unencrypted PDF.",
);
}
throw e;
}
const kinds: Record<string, FieldKind> = {};
const values: Record<string, FieldValue> = {};
const requiredByName: Record<string, boolean> = {};
for (let p = 1; p <= pdf.numPages; p++) {
const page = await pdf.getPage(p);
const annotations = (await page.getAnnotations()) as WidgetAnnotation[];
for (const a of annotations) {
const name = a.fieldName?.trim();
if (!name) continue;
const kind = kindFromAnnotation(a);
const v = valueFromAnnotation(a);
kinds[name] = kind;
values[name] = v;
if (isAnnotationRequired(a)) requiredByName[name] = true;
else if (requiredByName[name] === undefined) requiredByName[name] = false;
}
}
const available_fields = Object.keys(values).sort();
const required_fields = Object.keys(requiredByName)
.filter((n) => requiredByName[n])
.sort();
return { values, kinds, available_fields, required_fields };
}
export async function main(
nextcloud: RT.Nextcloud,
pdfPath: string,
pdfPassword: string | null = null,
): Promise<{
values: Record<string, FieldValue>;
available_fields: string[];
required_fields: string[];
filled_out: boolean;
}> {
const getRes = await axios.get(
`${String(nextcloud.baseUrl || "").replace(/\/$/, "")}/remote.php/dav/files/${encodeURIComponent(nextcloud.userId)}/${pdfPath}`,
{
auth: {
username: nextcloud.userId,
password: nextcloud.token,
},
responseType: "arraybuffer",
},
);
if (getRes.status !== 200) {
throw new Error(`Failed to download PDF (HTTP ${getRes.status}) ${getRes.statusText}`);
}
const pdfBytes = new Uint8Array(getRes.data as ArrayBuffer);
const { values, kinds, available_fields, required_fields } = await loadFormData(
pdfBytes,
pdfPassword ?? "",
);
const filled_out = required_fields.every((name) =>
isFilled(values[name], kinds[name] ?? "other"),
);
return { values, available_fields, required_fields, filled_out };
}
Submitted by nextcloud 23 days ago
import axios from "axios";
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
type FieldValue = string | boolean | null;
type FieldKind = "text" | "checkbox" | "other";
function trimStr(v: unknown): string {
return String(v ?? "").trim();
}
/** True if the field has a value that counts as “filled” for PDF-required validation. */
function isFilled(value: FieldValue, kind: FieldKind): boolean {
if (kind === "checkbox") return value === true || value === false;
if (kind === "text") {
return typeof value === "string" && value.trim().length > 0;
}
return value != null && String(value).trim().length > 0;
}
type WidgetAnnotation = {
fieldName?: string;
fieldValue?: string | null;
fieldType?: string;
checkBox?: boolean;
required?: boolean;
fieldFlags?: number;
};
const FIELD_FLAG_REQUIRED = 2;
function isAnnotationRequired(a: WidgetAnnotation): boolean {
if (typeof a.required === "boolean") return a.required;
const ff = a.fieldFlags;
return typeof ff === "number" && (ff & FIELD_FLAG_REQUIRED) !== 0;
}
function kindFromAnnotation(a: WidgetAnnotation): FieldKind {
const ft = a.fieldType;
if (ft === "Tx") return "text";
if (ft === "Btn") {
if (a.checkBox) return "checkbox";
return "other";
}
if (ft === "Ch") return "other";
return "other";
}
function valueFromAnnotation(a: WidgetAnnotation): FieldValue {
const ft = a.fieldType;
const raw = a.fieldValue;
if (ft === "Tx") return trimStr(raw ?? "");
if (ft === "Btn" && a.checkBox) {
const v = raw == null ? "" : String(raw);
if (v === "" || v === "Off") return false;
return true;
}
if (ft === "Ch") return trimStr(raw ?? "");
return null;
}
async function loadFormData(
pdfBytes: Uint8Array,
password: string,
): Promise<{
values: Record<string, FieldValue>;
kinds: Record<string, FieldKind>;
available_fields: string[];
required_fields: string[];
}> {
const loadingTask = getDocument({
data: pdfBytes,
password,
disableRange: true,
disableStream: true,
useSystemFonts: true,
});
let pdf: { numPages: number; getPage: (n: number) => Promise<{ getAnnotations: () => Promise<unknown[]> }> };
try {
pdf = await loadingTask.promise;
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (/password/i.test(msg)) {
throw new Error(
"PDF needs a password or a different one. Set `pdfPassword` or use an unencrypted PDF.",
);
}
throw e;
}
const kinds: Record<string, FieldKind> = {};
const values: Record<string, FieldValue> = {};
const requiredByName: Record<string, boolean> = {};
for (let p = 1; p <= pdf.numPages; p++) {
const page = await pdf.getPage(p);
const annotations = (await page.getAnnotations()) as WidgetAnnotation[];
for (const a of annotations) {
const name = a.fieldName?.trim();
if (!name) continue;
const kind = kindFromAnnotation(a);
const v = valueFromAnnotation(a);
kinds[name] = kind;
values[name] = v;
if (isAnnotationRequired(a)) requiredByName[name] = true;
else if (requiredByName[name] === undefined) requiredByName[name] = false;
}
}
const available_fields = Object.keys(values).sort();
const required_fields = Object.keys(requiredByName)
.filter((n) => requiredByName[n])
.sort();
return { values, kinds, available_fields, required_fields };
}
export async function main(
nextcloud: RT.Nextcloud,
pdfPath: string,
pdfPassword: string | null = null,
): Promise<{
values: Record<string, FieldValue>;
available_fields: string[];
required_fields: string[];
filled_out: boolean;
}> {
const getRes = await axios.get(
`${String(nextcloud.baseUrl || "").replace(/\/$/, "")}/remote.php/dav/files/${encodeURIComponent(nextcloud.userId)}/${pdfPath}`,
{
auth: {
username: nextcloud.userId,
password: nextcloud.token,
},
responseType: "arraybuffer",
},
);
if (getRes.status !== 200) {
throw new Error(`Failed to download PDF (HTTP ${getRes.status}) ${getRes.statusText}`);
}
const pdfBytes = new Uint8Array(getRes.data as ArrayBuffer);
const { values, kinds, available_fields, required_fields } = await loadFormData(
pdfBytes,
pdfPassword ?? "",
);
const filled_out = required_fields.every((name) =>
isFilled(values[name], kinds[name] ?? "other"),
);
return { values, available_fields, required_fields, filled_out };
}
Submitted by nextcloud 23 days ago