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);
}