feat: implement annotation flattening functionality and add tests
This commit is contained in:
@@ -1,231 +1,267 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { flattenAnnotations } from '../utils/flatten-annotations.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { FlattenPdfState } from '@/types';
|
||||
|
||||
const pageState: FlattenPdfState = {
|
||||
files: [],
|
||||
files: [],
|
||||
};
|
||||
|
||||
function flattenFormsInDoc(pdfDoc: PDFDocument) {
|
||||
const form = pdfDoc.getForm();
|
||||
form.flatten();
|
||||
const form = pdfDoc.getForm();
|
||||
form.flatten();
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.files = [];
|
||||
pageState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.files.length > 0) {
|
||||
pageState.files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
if (pageState.files.length > 0) {
|
||||
pageState.files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
pageState.files.splice(index, 1);
|
||||
updateUI();
|
||||
};
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className =
|
||||
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
pageState.files.splice(index, 1);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
createIcons({ icons });
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
}
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
||||
if (pdfFiles.length > 0) {
|
||||
pageState.files.push(...pdfFiles);
|
||||
updateUI();
|
||||
}
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(
|
||||
(f) =>
|
||||
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
if (pdfFiles.length > 0) {
|
||||
pageState.files.push(...pdfFiles);
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function flattenPdf() {
|
||||
if (pageState.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
if (pageState.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (pageState.files.length === 1) {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Flattening PDF...';
|
||||
try {
|
||||
if (pageState.files.length === 1) {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Flattening PDF...';
|
||||
|
||||
const file = pageState.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
|
||||
const file = pageState.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('getForm')) {
|
||||
// Ignore if no form found
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
|
||||
`flattened_${file.name}`
|
||||
);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('getForm')) {
|
||||
// Ignore if no form found
|
||||
} else {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Flattening multiple PDFs...';
|
||||
|
||||
const zip = new JSZip();
|
||||
let processedCount = 0;
|
||||
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
if (loaderText) loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('getForm')) {
|
||||
// Ignore if no form found
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const flattenedBytes = await pdfDoc.save();
|
||||
zip.file(`flattened_${file.name}`, flattenedBytes);
|
||||
processedCount++;
|
||||
} catch (e) {
|
||||
console.error(`Error processing ${file.name}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (processedCount > 0) {
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'flattened_pdfs.zip');
|
||||
showAlert('Success', `Processed ${processedCount} PDFs.`, 'success', () => { resetState(); });
|
||||
} else {
|
||||
showAlert('Error', 'No PDFs could be processed.');
|
||||
}
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
throw e;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert('Error', e.message || 'An unexpected error occurred.');
|
||||
}
|
||||
|
||||
try {
|
||||
flattenAnnotations(pdfDoc);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.warn('Could not flatten annotations:', msg);
|
||||
}
|
||||
|
||||
const newPdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
|
||||
`flattened_${file.name}`
|
||||
);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
} else {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Flattening multiple PDFs...';
|
||||
|
||||
const zip = new JSZip();
|
||||
let processedCount = 0;
|
||||
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
if (loaderText)
|
||||
loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('getForm')) {
|
||||
// Ignore if no form found
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
flattenAnnotations(pdfDoc);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.warn('Could not flatten annotations:', msg);
|
||||
}
|
||||
|
||||
const flattenedBytes = await pdfDoc.save();
|
||||
zip.file(`flattened_${file.name}`, flattenedBytes);
|
||||
processedCount++;
|
||||
} catch (e) {
|
||||
console.error(`Error processing ${file.name}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (processedCount > 0) {
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'flattened_pdfs.zip');
|
||||
showAlert(
|
||||
'Success',
|
||||
`Processed ${processedCount} PDFs.`,
|
||||
'success',
|
||||
() => {
|
||||
resetState();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
showAlert('Error', 'No PDFs could be processed.');
|
||||
}
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert('Error', e.message || 'An unexpected error occurred.');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files);
|
||||
});
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', flattenPdf);
|
||||
}
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', flattenPdf);
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', function () {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', function () {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
175
src/js/utils/flatten-annotations.ts
Normal file
175
src/js/utils/flatten-annotations.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import {
|
||||
PDFDocument,
|
||||
PDFName,
|
||||
PDFDict,
|
||||
PDFArray,
|
||||
PDFRef,
|
||||
PDFNumber,
|
||||
PDFOperator,
|
||||
PDFOperatorNames,
|
||||
} from 'pdf-lib';
|
||||
|
||||
function extractNumbers(arr: PDFArray, count: number): number[] | null {
|
||||
if (arr.size() < count) return null;
|
||||
const result: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const val = arr.lookup(i);
|
||||
if (!(val instanceof PDFNumber)) return null;
|
||||
result.push(val.asNumber());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolveStreamDict(obj: unknown): PDFDict | null {
|
||||
if (obj instanceof PDFDict) return obj;
|
||||
if (
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'dict' in (obj as Record<string, unknown>)
|
||||
) {
|
||||
const dict = (obj as { dict: unknown }).dict;
|
||||
if (dict instanceof PDFDict) return dict;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function flattenAnnotations(pdfDoc: PDFDocument): void {
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
for (const page of pages) {
|
||||
const pageNode = page.node;
|
||||
const annotsArr = pageNode.Annots();
|
||||
if (!annotsArr) continue;
|
||||
|
||||
const annotRefs = annotsArr.asArray();
|
||||
if (annotRefs.length === 0) continue;
|
||||
|
||||
const keptAnnots: PDFRef[] = [];
|
||||
let hasChanges = false;
|
||||
|
||||
for (const annotRef of annotRefs) {
|
||||
const annot = pdfDoc.context.lookup(annotRef);
|
||||
|
||||
if (!(annot instanceof PDFDict)) {
|
||||
if (annotRef instanceof PDFRef) keptAnnots.push(annotRef);
|
||||
continue;
|
||||
}
|
||||
|
||||
const subtype = annot.get(PDFName.of('Subtype'));
|
||||
const subtypeStr = subtype instanceof PDFName ? subtype.decodeText() : '';
|
||||
|
||||
if (subtypeStr === 'Widget') {
|
||||
if (annotRef instanceof PDFRef) keptAnnots.push(annotRef);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (subtypeStr === 'Popup') {
|
||||
hasChanges = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const flagsObj = annot.get(PDFName.of('F'));
|
||||
const flags = flagsObj instanceof PDFNumber ? flagsObj.asNumber() : 0;
|
||||
if (flags & 0x02) {
|
||||
hasChanges = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const apDict = annot.lookup(PDFName.of('AP'));
|
||||
if (!(apDict instanceof PDFDict)) {
|
||||
hasChanges = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let normalAppRef = apDict.get(PDFName.of('N'));
|
||||
if (!normalAppRef) {
|
||||
hasChanges = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalApp = pdfDoc.context.lookup(normalAppRef);
|
||||
if (normalApp instanceof PDFDict && !normalApp.has(PDFName.of('BBox'))) {
|
||||
const as = annot.get(PDFName.of('AS'));
|
||||
if (as instanceof PDFName && normalApp.has(as)) {
|
||||
normalAppRef = normalApp.get(as)!;
|
||||
} else {
|
||||
hasChanges = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const rectObj = annot.lookup(PDFName.of('Rect'));
|
||||
if (!(rectObj instanceof PDFArray)) {
|
||||
hasChanges = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const rectNums = extractNumbers(rectObj, 4);
|
||||
if (!rectNums) {
|
||||
hasChanges = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const x1 = Math.min(rectNums[0], rectNums[2]);
|
||||
const y1 = Math.min(rectNums[1], rectNums[3]);
|
||||
const x2 = Math.max(rectNums[0], rectNums[2]);
|
||||
const y2 = Math.max(rectNums[1], rectNums[3]);
|
||||
|
||||
if (x2 - x1 < 0.001 || y2 - y1 < 0.001) {
|
||||
hasChanges = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolvedStream = pdfDoc.context.lookup(normalAppRef);
|
||||
let bbox = [0, 0, x2 - x1, y2 - y1];
|
||||
|
||||
const streamDict = resolveStreamDict(resolvedStream);
|
||||
if (streamDict) {
|
||||
const bboxObj = streamDict.lookup(PDFName.of('BBox'));
|
||||
if (bboxObj instanceof PDFArray) {
|
||||
const bboxNums = extractNumbers(bboxObj, 4);
|
||||
if (bboxNums) bbox = bboxNums;
|
||||
}
|
||||
}
|
||||
|
||||
let appRef: PDFRef;
|
||||
if (normalAppRef instanceof PDFRef) {
|
||||
appRef = normalAppRef;
|
||||
} else {
|
||||
appRef = pdfDoc.context.register(normalAppRef as PDFDict);
|
||||
}
|
||||
const xObjKey = pageNode.newXObject('FlatAnnot', appRef);
|
||||
|
||||
const bw = bbox[2] - bbox[0];
|
||||
const bh = bbox[3] - bbox[1];
|
||||
const sx = Math.abs(bw) > 0.001 ? (x2 - x1) / bw : 1;
|
||||
const sy = Math.abs(bh) > 0.001 ? (y2 - y1) / bh : 1;
|
||||
const tx = x1 - bbox[0] * sx;
|
||||
const ty = y1 - bbox[1] * sy;
|
||||
|
||||
page.pushOperators(
|
||||
PDFOperator.of(PDFOperatorNames.PushGraphicsState),
|
||||
PDFOperator.of(PDFOperatorNames.ConcatTransformationMatrix, [
|
||||
PDFNumber.of(sx),
|
||||
PDFNumber.of(0),
|
||||
PDFNumber.of(0),
|
||||
PDFNumber.of(sy),
|
||||
PDFNumber.of(tx),
|
||||
PDFNumber.of(ty),
|
||||
]),
|
||||
PDFOperator.of(PDFOperatorNames.DrawObject, [xObjKey]),
|
||||
PDFOperator.of(PDFOperatorNames.PopGraphicsState)
|
||||
);
|
||||
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
if (keptAnnots.length > 0) {
|
||||
pageNode.set(PDFName.of('Annots'), pdfDoc.context.obj(keptAnnots));
|
||||
} else {
|
||||
pageNode.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { pdfSocket } from '../sockets';
|
||||
import type { SocketData } from '../types';
|
||||
import { requirePdfInput, processBatch } from '../types';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { flattenAnnotations } from '../../utils/flatten-annotations.js';
|
||||
|
||||
export class FlattenNode extends BaseWorkflowNode {
|
||||
readonly category = 'Secure PDF' as const;
|
||||
@@ -32,6 +33,12 @@ export class FlattenNode extends BaseWorkflowNode {
|
||||
console.error('Flatten form error (may have no forms):', err);
|
||||
}
|
||||
|
||||
try {
|
||||
flattenAnnotations(pdfDoc);
|
||||
} catch (err) {
|
||||
console.error('Flatten annotations error:', err);
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
return {
|
||||
type: 'pdf',
|
||||
|
||||
Reference in New Issue
Block a user