fix: replace iframe PDF rendering with direct canvas in form creator
This commit is contained in:
@@ -18,14 +18,6 @@ type LucideWindow = Window & {
|
|||||||
createIcons(): void;
|
createIcons(): void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
type PdfViewerApplicationLike = {
|
|
||||||
pdfViewer?: {
|
|
||||||
pagesCount: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
type PdfViewerWindow = Window & {
|
|
||||||
PDFViewerApplication?: PdfViewerApplicationLike;
|
|
||||||
};
|
|
||||||
|
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||||
@@ -68,8 +60,6 @@ let uploadedPdfjsDoc: PDFDocumentProxy | null = null;
|
|||||||
let uploadedFileName: string | null = null;
|
let uploadedFileName: string | null = null;
|
||||||
let pageSize: { width: number; height: number } = { width: 612, height: 792 };
|
let pageSize: { width: number; height: number } = { width: 612, height: 792 };
|
||||||
let currentScale = 1.333;
|
let currentScale = 1.333;
|
||||||
let pdfViewerOffset = { x: 0, y: 0 };
|
|
||||||
let pdfViewerScale = 1.333;
|
|
||||||
|
|
||||||
let resizing = false;
|
let resizing = false;
|
||||||
let resizeField: FormField | null = null;
|
let resizeField: FormField | null = null;
|
||||||
@@ -2228,23 +2218,11 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
const pdfPage = pdfDoc.getPage(field.pageIndex);
|
const pdfPage = pdfDoc.getPage(field.pageIndex);
|
||||||
const { height: pageHeight } = pdfPage.getSize();
|
const { height: pageHeight } = pdfPage.getSize();
|
||||||
|
|
||||||
const scaleX = 1 / pdfViewerScale;
|
const x = field.x / currentScale;
|
||||||
const scaleY = 1 / pdfViewerScale;
|
const y =
|
||||||
|
pageHeight - field.y / currentScale - field.height / currentScale;
|
||||||
const adjustedX = field.x - pdfViewerOffset.x;
|
const width = field.width / currentScale;
|
||||||
const adjustedY = field.y - pdfViewerOffset.y;
|
const height = field.height / currentScale;
|
||||||
|
|
||||||
const x = adjustedX * scaleX;
|
|
||||||
const y = pageHeight - adjustedY * scaleY - field.height * scaleY;
|
|
||||||
const width = field.width * scaleX;
|
|
||||||
const height = field.height * scaleY;
|
|
||||||
|
|
||||||
console.log(`Field "${field.name}":`, {
|
|
||||||
screenPos: { x: field.x, y: field.y },
|
|
||||||
adjustedPos: { x: adjustedX, y: adjustedY },
|
|
||||||
pdfPos: { x, y, width, height },
|
|
||||||
metrics: { offset: pdfViewerOffset, scale: pdfViewerScale },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (field.type === 'text') {
|
if (field.type === 'text') {
|
||||||
const textField = form.createTextField(field.name);
|
const textField = form.createTextField(field.name);
|
||||||
@@ -2862,141 +2840,34 @@ async function renderCanvas(): Promise<void> {
|
|||||||
|
|
||||||
canvas.innerHTML = '';
|
canvas.innerHTML = '';
|
||||||
|
|
||||||
if (uploadedPdfDoc) {
|
if (uploadedPdfjsDoc) {
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await uploadedPdfDoc.save();
|
const pdfjsPage = await uploadedPdfjsDoc.getPage(currentPageIndex + 1);
|
||||||
const blob = new Blob([arrayBuffer.buffer as ArrayBuffer], {
|
const viewport = pdfjsPage.getViewport({ scale: currentScale });
|
||||||
type: 'application/pdf',
|
|
||||||
});
|
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const iframe = document.createElement('iframe');
|
const pageCanvas = document.createElement('canvas');
|
||||||
iframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}#page=${currentPageIndex + 1}&toolbar=0`;
|
pageCanvas.width = viewport.width;
|
||||||
iframe.style.width = '100%';
|
pageCanvas.height = viewport.height;
|
||||||
iframe.style.height = `${canvasHeight}px`;
|
pageCanvas.style.position = 'absolute';
|
||||||
iframe.style.border = 'none';
|
pageCanvas.style.top = '0';
|
||||||
iframe.style.position = 'absolute';
|
pageCanvas.style.left = '0';
|
||||||
iframe.style.top = '0';
|
pageCanvas.style.pointerEvents = 'none';
|
||||||
iframe.style.left = '0';
|
|
||||||
iframe.style.pointerEvents = 'none';
|
|
||||||
iframe.style.opacity = '0.8';
|
|
||||||
|
|
||||||
iframe.onload = () => {
|
const ctx = pageCanvas.getContext('2d');
|
||||||
try {
|
if (ctx) {
|
||||||
const viewerWindow = iframe.contentWindow as PdfViewerWindow | null;
|
await pdfjsPage.render({
|
||||||
if (viewerWindow && viewerWindow.PDFViewerApplication) {
|
canvasContext: ctx,
|
||||||
const app = viewerWindow.PDFViewerApplication;
|
viewport,
|
||||||
|
canvas: pageCanvas,
|
||||||
const style = viewerWindow.document.createElement('style');
|
}).promise;
|
||||||
style.textContent = `
|
|
||||||
* {
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
}
|
||||||
html, body {
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
#toolbarContainer {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
#mainContainer {
|
|
||||||
top: 0 !important;
|
|
||||||
position: absolute !important;
|
|
||||||
left: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
#outerContainer {
|
|
||||||
background-color: transparent !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
#viewerContainer {
|
|
||||||
top: 0 !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.pdfViewer {
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
.page {
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
viewerWindow.document.head.appendChild(style);
|
|
||||||
|
|
||||||
const checkRender = setInterval(() => {
|
canvas.appendChild(pageCanvas);
|
||||||
if (app.pdfViewer && app.pdfViewer.pagesCount > 0) {
|
|
||||||
clearInterval(checkRender);
|
|
||||||
|
|
||||||
const pageContainer =
|
|
||||||
viewerWindow.document.querySelector<HTMLElement>('.page');
|
|
||||||
if (pageContainer) {
|
|
||||||
const initialRect = pageContainer.getBoundingClientRect();
|
|
||||||
|
|
||||||
const offsetX = -initialRect.left;
|
|
||||||
const offsetY = -initialRect.top;
|
|
||||||
pageContainer.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const rect = pageContainer.getBoundingClientRect();
|
|
||||||
const style = viewerWindow.getComputedStyle(pageContainer);
|
|
||||||
|
|
||||||
const borderLeft = parseFloat(style.borderLeftWidth) || 0;
|
|
||||||
const borderTop = parseFloat(style.borderTopWidth) || 0;
|
|
||||||
const borderRight = parseFloat(style.borderRightWidth) || 0;
|
|
||||||
|
|
||||||
pdfViewerOffset = {
|
|
||||||
x: rect.left + borderLeft,
|
|
||||||
y: rect.top + borderTop,
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentWidth = rect.width - borderLeft - borderRight;
|
|
||||||
pdfViewerScale = contentWidth / currentPage.width;
|
|
||||||
|
|
||||||
console.log('📏 Calibrated Metrics (force positioned):', {
|
|
||||||
initialPosition: {
|
|
||||||
left: initialRect.left,
|
|
||||||
top: initialRect.top,
|
|
||||||
},
|
|
||||||
appliedTransform: { x: offsetX, y: offsetY },
|
|
||||||
finalRect: {
|
|
||||||
left: rect.left,
|
|
||||||
top: rect.top,
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height,
|
|
||||||
},
|
|
||||||
computedBorders: {
|
|
||||||
left: borderLeft,
|
|
||||||
top: borderTop,
|
|
||||||
right: borderRight,
|
|
||||||
},
|
|
||||||
finalOffset: pdfViewerOffset,
|
|
||||||
finalScale: pdfViewerScale,
|
|
||||||
pdfDimensions: {
|
|
||||||
width: currentPage.width,
|
|
||||||
height: currentPage.height,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pendingFieldExtraction && uploadedPdfDoc) {
|
if (pendingFieldExtraction && uploadedPdfDoc) {
|
||||||
pendingFieldExtraction = false;
|
pendingFieldExtraction = false;
|
||||||
extractExistingFields(uploadedPdfDoc);
|
extractExistingFields(uploadedPdfDoc);
|
||||||
extractedFieldNames.forEach((name) =>
|
extractedFieldNames.forEach((name) => existingFieldNames.delete(name));
|
||||||
existingFieldNames.delete(name)
|
|
||||||
);
|
|
||||||
|
|
||||||
const form = uploadedPdfDoc.getForm();
|
const form = uploadedPdfDoc.getForm();
|
||||||
for (const name of extractedFieldNames) {
|
for (const name of extractedFieldNames) {
|
||||||
@@ -3016,27 +2887,6 @@ async function renderCanvas(): Promise<void> {
|
|||||||
renderCanvas();
|
renderCanvas();
|
||||||
updateFieldCount();
|
updateFieldCount();
|
||||||
}
|
}
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error accessing iframe content:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
canvas.appendChild(iframe);
|
|
||||||
|
|
||||||
console.log('Canvas dimensions:', {
|
|
||||||
width: canvasWidth,
|
|
||||||
height: canvasHeight,
|
|
||||||
scale: currentScale,
|
|
||||||
});
|
|
||||||
console.log('PDF page dimensions:', {
|
|
||||||
width: currentPage.width,
|
|
||||||
height: currentPage.height,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rendering PDF:', error);
|
console.error('Error rendering PDF:', error);
|
||||||
}
|
}
|
||||||
@@ -3115,8 +2965,8 @@ function extractExistingFields(pdfDoc: PDFDocument): void {
|
|||||||
pdfDoc,
|
pdfDoc,
|
||||||
fieldCounterStart: fieldCounter,
|
fieldCounterStart: fieldCounter,
|
||||||
metrics: {
|
metrics: {
|
||||||
pdfViewerOffset,
|
pdfViewerOffset: { x: 0, y: 0 },
|
||||||
pdfViewerScale,
|
pdfViewerScale: currentScale,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import {
|
|||||||
import { extractExistingFields } from '../js/logic/form-creator-extraction.ts';
|
import { extractExistingFields } from '../js/logic/form-creator-extraction.ts';
|
||||||
import type { ExtractedFieldLike } from '@/types';
|
import type { ExtractedFieldLike } from '@/types';
|
||||||
|
|
||||||
|
const FORM_CREATOR_SCALE = 1.333;
|
||||||
|
|
||||||
const TEST_EXTRACTION_METRICS = {
|
const TEST_EXTRACTION_METRICS = {
|
||||||
pdfViewerOffset: { x: 0, y: 0 },
|
pdfViewerOffset: { x: 0, y: 0 },
|
||||||
pdfViewerScale: 1,
|
pdfViewerScale: FORM_CREATOR_SCALE,
|
||||||
};
|
};
|
||||||
|
|
||||||
function extractFieldsForTest(pdfDoc: PDFDocument): ExtractedFieldLike[] {
|
function extractFieldsForTest(pdfDoc: PDFDocument): ExtractedFieldLike[] {
|
||||||
@@ -377,12 +379,13 @@ describe('form creator extraction regression', () => {
|
|||||||
const field = extracted.find((entry) => entry.name === 'page2TextField');
|
const field = extracted.find((entry) => entry.name === 'page2TextField');
|
||||||
|
|
||||||
expect(field).toBeDefined();
|
expect(field).toBeDefined();
|
||||||
|
const pageHeight = pdfDoc.getPages()[1].getSize().height;
|
||||||
expect(field).toMatchObject({
|
expect(field).toMatchObject({
|
||||||
pageIndex: 1,
|
pageIndex: 1,
|
||||||
x: rect.x,
|
x: rect.x * FORM_CREATOR_SCALE,
|
||||||
y: pdfDoc.getPages()[1].getSize().height - rect.y - rect.height,
|
y: (pageHeight - rect.y - rect.height) * FORM_CREATOR_SCALE,
|
||||||
width: rect.width,
|
width: rect.width * FORM_CREATOR_SCALE,
|
||||||
height: rect.height,
|
height: rect.height * FORM_CREATOR_SCALE,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -411,4 +414,34 @@ describe('form creator extraction regression', () => {
|
|||||||
expect(pageMap.get('page1ExtraField')).toBe(0);
|
expect(pageMap.get('page1ExtraField')).toBe(0);
|
||||||
expect(pageMap.get('page2TextField')).toBe(1);
|
expect(pageMap.get('page2TextField')).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('round-trips extracted canvas coords back to original PDF coords within 1pt', async () => {
|
||||||
|
const { pdfDoc } = await buildTwoPageTextFieldPdf();
|
||||||
|
|
||||||
|
const page = pdfDoc.getPages()[0];
|
||||||
|
const { height: pageHeight } = page.getSize();
|
||||||
|
|
||||||
|
const originalWidget = pdfDoc
|
||||||
|
.getForm()
|
||||||
|
.getTextField('page1TextField')
|
||||||
|
.acroField.getWidgets()[0];
|
||||||
|
const originalRect = originalWidget.getRectangle();
|
||||||
|
|
||||||
|
const extracted = extractFieldsForTest(pdfDoc);
|
||||||
|
const field = extracted.find((f) => f.name === 'page1TextField');
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
|
||||||
|
const pdfX = field!.x / FORM_CREATOR_SCALE;
|
||||||
|
const pdfY =
|
||||||
|
pageHeight -
|
||||||
|
field!.y / FORM_CREATOR_SCALE -
|
||||||
|
field!.height / FORM_CREATOR_SCALE;
|
||||||
|
const pdfW = field!.width / FORM_CREATOR_SCALE;
|
||||||
|
const pdfH = field!.height / FORM_CREATOR_SCALE;
|
||||||
|
|
||||||
|
expect(pdfX).toBeCloseTo(originalRect.x, 0);
|
||||||
|
expect(pdfY).toBeCloseTo(originalRect.y, 0);
|
||||||
|
expect(pdfW).toBeCloseTo(originalRect.width, 0);
|
||||||
|
expect(pdfH).toBeCloseTo(originalRect.height, 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user