import html2canvas from "html2canvas"; import { jsPDF } from "jspdf"; const fontFile = "Vazirmatn-Regular.ttf"; const fontFamily = "VazirmatnReceipt"; const pageMargin = 24; const pdfCaptureScale = 3; const jpegQuality = 0.94; const ignoredReceiptControls = ".registration-copy-reference, .registration-event-details-link"; const omittedReceiptContent = `${ignoredReceiptControls}, [data-receipt-status-badge], [data-receipt-reference]`; function toBase64(buffer: ArrayBuffer) { const bytes = new Uint8Array(buffer); let binary = ""; for (let offset = 0; offset < bytes.length; offset += 0x8000) { binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000)); } return window.btoa(binary); } function toUtf16Hex(value: string) { let result = "FEFF"; for (let index = 0; index < value.length; index += 1) { result += value.charCodeAt(index).toString(16).padStart(4, "0"); } return result.toUpperCase(); } async function addSelectableTextLayer( pdf: jsPDF, element: HTMLElement, canvas: HTMLCanvasElement, image: { x: number; y: number; scale: number }, ) { const response = await fetch(`/fonts/${fontFile}`, { cache: "force-cache" }); if (!response.ok) throw new Error("فونت رسید بارگذاری نشد."); const base64 = toBase64(await response.arrayBuffer()); pdf.addFileToVFS(fontFile, base64); pdf.addFont(fontFile, fontFamily, "normal"); pdf.setFont(fontFamily, "normal"); pdf.setR2L(false); (pdf as unknown as { __private__: { setPdfVersion: (version: string) => void } }).__private__.setPdfVersion("1.4"); const rootRect = element.getBoundingClientRect(); const captureScale = canvas.width / rootRect.width; const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); let node: Node | null = walker.nextNode(); const write = (pdf.internal as unknown as { write: (value: string) => void }).write; while (node) { const textNode = node as Text; const parent = textNode.parentElement; const value = textNode.nodeValue ?? ""; if ( parent && value.trim() && !parent.closest(`${omittedReceiptContent}, button, svg, [aria-hidden="true"]`) && getComputedStyle(parent).display !== "none" && getComputedStyle(parent).visibility !== "hidden" ) { const words = value.matchAll(/\S+/gu); for (const match of words) { const word = match[0]; const offset = match.index; if (!word) continue; const range = document.createRange(); range.setStart(textNode, offset); range.setEnd(textNode, offset + word.length); const rect = range.getBoundingClientRect(); if (!rect.width || !rect.height) continue; const computedStyle = getComputedStyle(parent); const fontSize = Number.parseFloat(computedStyle.fontSize) || 14; const x = image.x + (rect.left - rootRect.left) * captureScale * image.scale; const y = image.y + (rect.bottom - rootRect.top) * captureScale * image.scale; const pdfFontSize = Math.max(4, fontSize * 0.75 * captureScale * image.scale); const following = value.slice(offset + word.length, offset + word.length + 1); const actualText = `${word}${following && /\s/u.test(following) ? " " : ""}`; // The page image keeps the original visual layout; this invisible layer preserves selectable Unicode text. write(`/Span << /ActualText <${toUtf16Hex(actualText)}> >> BDC`); pdf.setFontSize(pdfFontSize); pdf.text(word, x, y, { align: "left", renderingMode: "invisible" }); write("EMC"); } } node = walker.nextNode(); } } async function prepareReceiptImage(image: HTMLImageElement) { const source = image.currentSrc || image.src; if (!source) return; const parsedSource = new URL(source, window.location.href); if (!["http:", "https:"].includes(parsedSource.protocol)) { await image.decode(); return; } image.removeAttribute("srcset"); const encodeForReceipt = async (imageBlob: Blob) => { if (!imageBlob.type.startsWith("image/")) throw new Error("پاسخ تصویر رویداد معتبر نیست."); const bitmap = await createImageBitmap(imageBlob); const resize = Math.min(1, 640 / Math.max(bitmap.width, bitmap.height)); const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.round(bitmap.width * resize)); canvas.height = Math.max(1, Math.round(bitmap.height * resize)); const context = canvas.getContext("2d"); if (!context) { bitmap.close(); throw new Error("آماده‌سازی تصویر رویداد برای رسید انجام نشد."); } context.fillStyle = "#ffffff"; context.fillRect(0, 0, canvas.width, canvas.height); context.drawImage(bitmap, 0, 0, canvas.width, canvas.height); bitmap.close(); return canvas.toDataURL("image/jpeg", 0.88); }; try { const response = await fetch(parsedSource.href, { mode: "cors", credentials: parsedSource.origin === window.location.origin ? "same-origin" : "omit", }); if (!response.ok) throw new Error("تصویر رویداد برای رسید بارگذاری نشد."); image.src = await encodeForReceipt(await response.blob()); } catch { if (parsedSource.origin === window.location.origin) { throw new Error("تصویر رویداد برای رسید بارگذاری نشد."); } const optimizedUrl = new URL("/_next/image", window.location.origin); optimizedUrl.searchParams.set("url", parsedSource.href); optimizedUrl.searchParams.set("w", "640"); optimizedUrl.searchParams.set("q", "75"); const response = await fetch(optimizedUrl, { credentials: "same-origin" }); if (!response.ok) throw new Error("تصویر رویداد برای رسید بارگذاری نشد."); image.src = await encodeForReceipt(await response.blob()); } await image.decode(); } export async function downloadRegistrationReceipt(element: HTMLElement, fileName: string) { await document.fonts.ready; const canvas = await html2canvas(element, { scale: pdfCaptureScale, useCORS: true, allowTaint: false, backgroundColor: "#ffffff", onclone: async (documentClone, clonedElement) => { documentClone.documentElement.classList.remove("dark"); clonedElement.querySelectorAll(omittedReceiptContent).forEach((control) => control.remove()); const receiptHeader = clonedElement.querySelector("header"); if (receiptHeader) { receiptHeader.style.alignItems = "start"; const headerText = receiptHeader.firstElementChild as HTMLElement | null; if (headerText) headerText.style.alignSelf = "start"; } const eventCard = clonedElement.querySelector("section h2")?.closest("section"); if (eventCard) { (eventCard as HTMLElement).style.alignItems = "start"; const eventText = eventCard.firstElementChild as HTMLElement | null; if (eventText) eventText.style.alignSelf = "start"; } const images = Array.from(clonedElement.querySelectorAll("img")); await Promise.all(images.map(prepareReceiptImage)); }, }); const pdf = new jsPDF({ orientation: "portrait", unit: "pt", format: "a4", compress: true, putOnlyUsedFonts: true, }); const pageWidth = pdf.internal.pageSize.getWidth(); const pageHeight = pdf.internal.pageSize.getHeight(); const scale = Math.min( (pageWidth - pageMargin * 2) / canvas.width, (pageHeight - pageMargin * 2) / canvas.height, ); const width = canvas.width * scale; const height = canvas.height * scale; const x = (pageWidth - width) / 2; const y = pageMargin; const compressedImage = canvas.toDataURL("image/jpeg", jpegQuality); pdf.setFillColor(255, 255, 255); pdf.rect(0, 0, pageWidth, pageHeight, "F"); pdf.addImage(compressedImage, "JPEG", x, y, width, height, undefined, "FAST"); await addSelectableTextLayer(pdf, element, canvas, { x, y, scale }); pdf.setProperties({ title: "رسید ثبت‌نام رویداد", subject: "رسید ثبت‌نام", creator: "سامانهٔ انجمن" }); pdf.save(fileName); }