(() => { const { Icon, IconButton } = window.TaskFlowDesignSystem_dd53c2; const PDFJS_URL = "https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.min.js"; const WORKER_URL = "https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js"; let pdfjsPromise = null; /* pdf.js dipakai agar isi PDF benar-benar tampil (plugin PDF bawaan browser sering diblokir di dalam iframe/webview). Halaman dirender ke . */ const loadPdfJs = () => { if (window.pdfjsLib) return Promise.resolve(window.pdfjsLib); if (pdfjsPromise) return pdfjsPromise; pdfjsPromise = new Promise((res, rej) => { const s = document.createElement("script"); s.src = PDFJS_URL; s.onload = () => { window.pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_URL; res(window.pdfjsLib); }; s.onerror = rej; document.head.appendChild(s); }); return pdfjsPromise; }; function PdfCanvas({ src, page = 1, onPages, onSize, height = 460, fit = "page" }) { const wrapRef = React.useRef(null); const canvasRef = React.useRef(null); const docRef = React.useRef(null); const taskRef = React.useRef(null); const sizeRef = React.useRef({ w: 0, h: 0 }); const [state, setState] = React.useState("loading"); const [tick, setTick] = React.useState(0); React.useEffect(() => { let dead = false; setState("loading"); loadPdfJs() .then((lib) => lib.getDocument(src).promise) .then((doc) => { if (dead) return; docRef.current = doc; if (onPages) onPages(doc.numPages); setState("ready"); }) .catch(() => !dead && setState("error")); return () => { dead = true; docRef.current = null; }; }, [src]); React.useEffect(() => { const doc = docRef.current; if (state !== "ready" || !doc) return; let dead = false; // Render sebelumnya WAJIB dibatalkan: dua render pada satu canvas membuat // halaman tampak terbalik/rusak (penyebab tampilan hancur saat menaruh TTD). if (taskRef.current) { try { taskRef.current.cancel(); } catch (e) {} taskRef.current = null; } doc.getPage(Math.min(Math.max(1, page), doc.numPages)).then((pg) => { if (dead || !canvasRef.current) return; const boxW = (wrapRef.current && wrapRef.current.clientWidth) || 460; const base = pg.getViewport({ scale: 1 }); // fit="page": satu halaman utuh terlihat tanpa scroll, apa pun ukuran/orientasi PDF. const css = fit === "page" ? Math.min(boxW / base.width, height / base.height) : boxW / base.width; const vp = pg.getViewport({ scale: Math.max(0.1, css) * (window.devicePixelRatio || 1) }); const cv = canvasRef.current; const cw = Math.round(base.width * css); const ch = Math.round(base.height * css); cv.width = Math.round(vp.width); cv.height = Math.round(vp.height); cv.style.width = cw + "px"; cv.style.height = ch + "px"; const ctx = cv.getContext("2d"); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, cv.width, cv.height); const t = pg.render({ canvasContext: ctx, viewport: vp }); taskRef.current = t; t.promise.then(() => { if (dead) return; taskRef.current = null; // Ukuran dilaporkan SETELAH render selesai dan hanya bila berubah, // supaya tidak memicu resize → render berulang tanpa henti. if (onSize && (sizeRef.current.w !== cw || sizeRef.current.h !== ch)) { sizeRef.current = { w: cw, h: ch }; onSize({ w: cw, h: ch }); } }).catch(() => {}); }); return () => { dead = true; if (taskRef.current) { try { taskRef.current.cancel(); } catch (e) {} taskRef.current = null; } }; }, [state, page, src, height, fit, tick]); React.useEffect(() => { if (typeof ResizeObserver === "undefined" || !wrapRef.current) return; let timer = null; const ro = new ResizeObserver(() => { clearTimeout(timer); timer = setTimeout(() => setTick((t) => t + 1), 160); // redam agar tidak render berulang }); ro.observe(wrapRef.current); return () => { clearTimeout(timer); ro.disconnect(); }; }, []); return (
{state !== "ready" && ( {state === "error" ? "PDF gagal dirender — pakai tombol Buka di Tab Baru / Download." : "Memuat dokumen…"} )}
); } /* Unduh dokumen DENGAN tanda tangan menyatu (bukan file asli). PDF: tiap halaman dirender pdf.js → JPEG, TTD digambar di halaman terpilih, lalu semua halaman dirangkai kembali jadi satu PDF. Gambar: cukup satu kanvas. */ const jpegOf = (cv) => cv.toDataURL("image/jpeg", 0.9).split(",")[1]; const buildPdf = (pages) => { // pages: [{ b64, w, h }] — satu XObject JPEG per halaman. const objs = []; const n = pages.length; objs.push("<>"); objs.push("< (3 + i * 3) + " 0 R").join(" ") + "]/Count " + n + ">>"); pages.forEach((pg, i) => { const base = 3 + i * 3; objs.push("<>>>/Contents " + (base + 1) + " 0 R>>"); const st = "q " + pg.w + " 0 0 " + pg.h + " 0 0 cm /I0 Do Q"; objs.push("<>stream\n" + st + "\nendstream"); const bin = atob(pg.b64); objs.push("<>stream\n" + bin + "\nendstream"); }); let out = "%PDF-1.4\n"; const off = []; objs.forEach((o, i) => { off.push(out.length); out += (i + 1) + " 0 obj" + o + "endobj\n"; }); const xref = out.length; out += "xref\n0 " + (objs.length + 1) + "\n0000000000 65535 f \n" + off.map((o) => String(o).padStart(10, "0") + " 00000 n \n").join(""); out += "trailer<>\nstartxref\n" + xref + "\n%%EOF"; const arr = new Uint8Array(out.length); for (let i = 0; i < out.length; i++) arr[i] = out.charCodeAt(i) & 0xff; return new Blob([arr], { type: "application/pdf" }); }; const loadImg = (src) => new Promise((res, rej) => { const i = new Image(); i.onload = () => res(i); i.onerror = rej; i.src = src; }); const drawSig = (ctx, sigImg, sig, w, h) => { const sw = (sig.width / 100) * w; const sh = sw * (sigImg.height / sigImg.width); ctx.drawImage(sigImg, (sig.x / 100) * w - sw / 2, (sig.y / 100) * h - sh / 2, sw, sh); }; async function downloadSigned(file, sigUrl, sig, name) { const src = window.fileSrc ? window.fileSrc(file) : file.url; const sigImg = sigUrl ? await loadImg(sigUrl) : null; const isImg = file.kind === "image" || /\.(png|jpe?g|webp)$/i.test(file.name || ""); let blob; if (isImg) { const im = await loadImg(src); const cv = document.createElement("canvas"); cv.width = im.width; cv.height = im.height; const ctx = cv.getContext("2d"); ctx.drawImage(im, 0, 0); if (sigImg && sig) drawSig(ctx, sigImg, sig, cv.width, cv.height); blob = await new Promise((r) => cv.toBlob(r, "image/jpeg", 0.92)); name = String(name).replace(/\.[a-z0-9]+$/i, "") + ".jpg"; } else { const lib = await loadPdfJs(); const doc = await lib.getDocument(src).promise; const out = []; for (let i = 1; i <= doc.numPages; i++) { const pg = await doc.getPage(i); const base = pg.getViewport({ scale: 1 }); const vp = pg.getViewport({ scale: 2 }); const cv = document.createElement("canvas"); cv.width = Math.round(vp.width); cv.height = Math.round(vp.height); const ctx = cv.getContext("2d"); ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, cv.width, cv.height); await pg.render({ canvasContext: ctx, viewport: vp }).promise; // Halaman TTD diklem ke jumlah halaman nyata (dokumen lama bisa menyimpan nomor > jumlah halaman). if (sigImg && sig && Math.min(Math.max(1, sig.page || 1), doc.numPages) === i) drawSig(ctx, sigImg, sig, cv.width, cv.height); out.push({ b64: jpegOf(cv), w: Math.round(base.width), h: Math.round(base.height), iw: cv.width, ih: cv.height }); } blob = buildPdf(out); } const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 8000); } Object.assign(window, { PdfCanvas, loadPdfJs, downloadSigned }); })();