Update edit.html
Browse files
edit.html
CHANGED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>PDFix – Online PDF Editor</title>
|
| 7 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/@pdf-lib/fontkit@1.1.1/dist/fontkit.umd.min.js"></script>
|
| 10 |
+
<style>
|
| 11 |
+
:root{--ink:#172033;--muted:#667085;--line:#d9e1ee;--blue:#2864e8;--soft:#e8f0ff;--red:#c93434}
|
| 12 |
+
*{box-sizing:border-box}body{margin:0;font-family:Inter,Segoe UI,Arial,sans-serif;color:var(--ink);background:#e9edf3}
|
| 13 |
+
button,input,select{font:inherit}
|
| 14 |
+
header{background:#fff;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20;padding:10px 16px;display:flex;flex-direction:column;align-items:center;gap:8px}
|
| 15 |
+
.top{width:100%;display:flex;align-items:center;justify-content:space-between}.brand{display:flex;align-items:center;gap:9px;font-size:19px;font-weight:800}.logo{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#2864e8,#7c4dff);display:grid;place-items:center;color:#fff;font-size:16px}
|
| 16 |
+
.btn{cursor:pointer;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:8px;padding:7px 12px;font-weight:600;font-size:13px}.btn:hover{border-color:#9ab5f7}.btn.primary{background:var(--blue);color:#fff;border-color:var(--blue)}.btn.danger{color:var(--red)}.btn:disabled{opacity:.4;cursor:not-allowed}
|
| 17 |
+
.tb{display:flex;flex-wrap:wrap;justify-content:center;border:1px solid var(--blue);border-radius:8px;overflow:hidden;background:#fff}
|
| 18 |
+
.tb button{cursor:pointer;border:0;border-right:1px solid #c9d8fb;background:#fff;color:#1f3b8c;padding:9px 14px;font-size:14px;display:flex;align-items:center;gap:6px}.tb button:last-child{border-right:0}.tb button:hover{background:#f3f7ff}.tb button.active{background:var(--soft);font-weight:700}.tb button:disabled{opacity:.4;cursor:not-allowed}
|
| 19 |
+
.bar{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);flex-wrap:wrap;justify-content:center}.bar .btn{padding:4px 9px;font-size:12px}
|
| 20 |
+
.status{text-align:center;font-size:12px;color:var(--muted);padding:6px;background:#f7f9fc;border-bottom:1px solid var(--line)}
|
| 21 |
+
.cv{overflow:auto;padding:24px 12px 80px;min-height:calc(100vh - 150px)}
|
| 22 |
+
.welcome{max-width:520px;margin:6vh auto;text-align:center;background:#fff;border:1px solid var(--line);border-radius:18px;padding:40px 28px}.welcome h1{font-size:25px;margin:10px 0}.welcome p{color:var(--muted);line-height:1.6}
|
| 23 |
+
.page-wrap{position:relative;margin:0 auto;background:#fff;box-shadow:0 6px 28px rgba(22,34,61,.2)}.page-wrap canvas{display:block}
|
| 24 |
+
.overlay{position:absolute;inset:0;overflow:hidden;touch-action:none}.overlay.t{cursor:text}.overlay.c{cursor:crosshair}
|
| 25 |
+
.blk{position:absolute;border:1px dashed transparent;cursor:text}.blk:hover{border-color:#5b8cf5;background:rgba(40,100,232,.1)}
|
| 26 |
+
.item{position:absolute;cursor:move}.item.sel{outline:1px dashed var(--blue);outline-offset:2px}
|
| 27 |
+
.item.text{white-space:pre;min-width:8px}.item.editing{outline:2px solid var(--blue);background:rgba(255,255,255,.95);cursor:text;min-width:24px}.item.editing:empty:before{content:'Type here';color:#9aa5b8}
|
| 28 |
+
.item.highlight{mix-blend-mode:multiply;opacity:.45}.item.rect{border:2px solid}.item img{width:100%;height:100%;pointer-events:none}.ghost{position:absolute;border:1px dashed var(--blue);background:rgba(40,100,232,.1);pointer-events:none}
|
| 29 |
+
.hd{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;background:var(--blue);cursor:nwse-resize;border-radius:3px}
|
| 30 |
+
.ft{position:absolute;z-index:10;display:none;align-items:center;gap:2px;background:#fff;border:1px solid var(--blue);border-radius:8px;box-shadow:0 6px 20px rgba(22,34,61,.2);padding:3px;line-height:normal;white-space:nowrap}
|
| 31 |
+
.ft button{cursor:pointer;border:0;background:#fff;color:#1f3b8c;min-width:34px;height:34px;border-radius:6px;font-size:15px}.ft button:hover{background:#f3f7ff}.ft button.on{background:var(--soft)}
|
| 32 |
+
.ft input[type=number]{width:52px;height:30px;border:1px solid var(--line);border-radius:6px;text-align:center}.ft select{height:30px;border:1px solid var(--line);border-radius:6px;max-width:130px}.ft input[type=color]{width:34px;height:30px;border:1px solid var(--line);border-radius:6px;padding:1px;background:#fff}
|
| 33 |
+
.tl{position:absolute;inset:0;overflow:hidden;line-height:1;pointer-events:none}.tl.on{pointer-events:auto}
|
| 34 |
+
.tl span,.tl br{color:transparent;position:absolute;white-space:pre;cursor:text;transform-origin:0% 0%}.tl ::selection{background:rgba(40,100,232,.35)}
|
| 35 |
+
.overlay.s,.overlay.s *{pointer-events:none}
|
| 36 |
+
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#172033;color:#fff;padding:11px 16px;border-radius:10px;font-size:13px;z-index:30;display:none;max-width:90vw}
|
| 37 |
+
</style>
|
| 38 |
+
</head>
|
| 39 |
+
<body>
|
| 40 |
+
<header>
|
| 41 |
+
<div class="top">
|
| 42 |
+
<div class="brand"><div class="logo">P</div>PDFix</div>
|
| 43 |
+
<div style="display:flex;gap:8px"><button class="btn" id="openBtn">Open PDF</button><button class="btn primary" id="saveBtn" disabled>Export PDF</button></div>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="tb" id="tb">
|
| 46 |
+
<button data-mode="text" class="active">𝙸A Text</button>
|
| 47 |
+
<button data-mode="select">⌶ Select / Copy</button>
|
| 48 |
+
<button data-mode="image">▧ Image</button>
|
| 49 |
+
<button data-mode="cover">◇ Whiteout</button>
|
| 50 |
+
<button data-mode="highlight">🖍 Highlight</button>
|
| 51 |
+
<button data-mode="draw">✎ Draw</button>
|
| 52 |
+
<button data-mode="rect">▱ Rectangle</button>
|
| 53 |
+
<button id="fontBtn">Aa Fonts</button>
|
| 54 |
+
<button id="undoBtn" disabled>⎌ Undo</button>
|
| 55 |
+
</div>
|
| 56 |
+
<div class="bar">
|
| 57 |
+
<button class="btn" id="prevBtn" disabled>←</button><span id="pageLabel">No PDF open</span><button class="btn" id="nextBtn" disabled>→</button>
|
| 58 |
+
<span>|</span><button class="btn" id="zoomOut" disabled>−</button><span id="zoomLabel">125%</span><button class="btn" id="zoomIn" disabled>+</button><button class="btn" id="fitBtn" disabled>Fit width</button>
|
| 59 |
+
<span>|</span><button class="btn danger" id="delPage" disabled>Delete page</button>
|
| 60 |
+
<span id="fontNames" title="Custom fonts loaded">Fonts: none</span>
|
| 61 |
+
</div>
|
| 62 |
+
</header>
|
| 63 |
+
<input id="fileInput" type="file" accept="application/pdf,.pdf" hidden><input id="imageInput" type="file" accept="image/png,image/jpeg" hidden><input id="fontInput" type="file" accept=".ttf,.otf" multiple hidden>
|
| 64 |
+
<div class="status" id="status">Open a PDF, then click any text to edit it. Click empty space to add new text.</div>
|
| 65 |
+
<div class="cv" id="canvasArea">
|
| 66 |
+
<div class="welcome" id="welcome"><div style="font-size:50px">📄</div><h1>Edit PDF online</h1><p>Click text to edit it, move text boxes, and add images or shapes. Font matching depends on the PDF's embedded fonts; for the closest match, upload the original TTF/OTF font using Aa Fonts.</p><button class="btn primary" id="welcomeOpen" style="padding:11px 22px;font-size:15px">Choose a PDF</button></div>
|
| 67 |
+
</div>
|
| 68 |
+
<div id="toast" class="toast"></div>
|
| 69 |
+
<script>
|
| 70 |
+
pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
| 71 |
+
const {PDFDocument,rgb,StandardFonts}=PDFLib,$=i=>document.getElementById(i);
|
| 72 |
+
const FF={sans:'Arial,Helvetica,sans-serif',serif:'"Times New Roman",Times,serif',mono:'"Courier New",Courier,monospace'};
|
| 73 |
+
const FN={sans:['Helvetica','HelveticaBold','HelveticaOblique','HelveticaBoldOblique'],serif:['TimesRoman','TimesRomanBold','TimesRomanItalic','TimesRomanBoldItalic'],mono:['Courier','CourierBold','CourierOblique','CourierBoldOblique']};
|
| 74 |
+
const MSG={select:'Select words with the mouse, then press Ctrl+C to copy. Double-click selects a word, triple-click a line.',text:'Click any text to edit it. Click empty space to add text. Drag an object to move it.',image:'Choose an image to insert.',cover:'Drag over content to whiteout it.',highlight:'Drag to highlight.',draw:'Draw freely. Click the tool again to stop.',rect:'Drag to draw a rectangle.'};
|
| 75 |
+
const COL={highlight:'#ffeb3b',rect:'#2864e8',cover:'#ffffff',draw:'#2864e8'};
|
| 76 |
+
const ASC=.945,CF={};
|
| 77 |
+
let curPg,tl,bytes,doc,page=1,pages=0,zoom=1.25,mode='text',sel=null,editing=null,objs=[],undo=[],W=0,H=0,cv,ov,blocks=[],drawId=0;
|
| 78 |
+
const isC=f=>f.startsWith('c:'),BL=f=>CF[f]?CF[f].bl:ASC,uid=()=>Math.random().toString(36).slice(2),cur=()=>objs.find(o=>o.id===sel);
|
| 79 |
+
const fcss=o=>isC(o.family)&&CF[o.family]?`"${CF[o.family].name}",${FF[o.fb]||FF.sans}`:FF[o.family]||FF.sans;
|
| 80 |
+
const stk=o=>JSON.stringify([o.family,o.size,o.bold,o.italic,o.color]);
|
| 81 |
+
const toast=m=>{const t=$('toast');t.textContent=m;t.style.display='block';clearTimeout(toast.t);toast.t=setTimeout(()=>t.style.display='none',3200)};
|
| 82 |
+
const status=s=>$('status').textContent=s;
|
| 83 |
+
const snap=()=>{undo.push(JSON.stringify(objs));if(undo.length>40)undo.shift();$('undoBtn').disabled=false};
|
| 84 |
+
const xy=e=>{const r=ov.getBoundingClientRect();return{x:(e.clientX-r.left)/zoom,y:(e.clientY-r.top)/zoom}};
|
| 85 |
+
|
| 86 |
+
/* ---------- floating toolbar (like Sejda) ---------- */
|
| 87 |
+
const ft=document.createElement('div');ft.className='ft';
|
| 88 |
+
ft.innerHTML=`<button data-a="B" class="tx"><b>B</b></button><button data-a="I" class="tx"><i>I</i></button><button data-a="m" class="tx" title="Smaller">A−</button><input data-a="S" class="tx" type="number" min="4" max="200" step="0.5"><button data-a="p" class="tx" title="Bigger">A+</button><select data-a="F" class="tx"><option value="sans">Helvetica</option><option value="serif">Times</option><option value="mono">Courier</option></select><input data-a="C" type="color" title="Colour"><button data-a="D" title="Duplicate">⧉</button><button data-a="X" title="Delete">🗑</button>`;
|
| 89 |
+
const fq=a=>ft.querySelector(`[data-a="${a}"]`);
|
| 90 |
+
ft.onpointerdown=e=>{if(!/INPUT|SELECT/.test(e.target.tagName))e.preventDefault()};
|
| 91 |
+
const upd=fn=>{const o=cur();if(!o)return;fn(o);render()};
|
| 92 |
+
ft.onclick=e=>{const a=e.target.closest('button')?.dataset.a;if(!a)return;
|
| 93 |
+
if(a==='B')upd(o=>o.bold=!o.bold);if(a==='I')upd(o=>o.italic=!o.italic);
|
| 94 |
+
if(a==='m')upd(o=>{o.size=Math.max(4,+(o.size-1).toFixed(1));o.h=o.size*1.2});if(a==='p')upd(o=>{o.size=+(o.size+1).toFixed(1);o.h=o.size*1.2});
|
| 95 |
+
if(a==='D'){const o=cur();if(!o)return;endEdit();snap();const c=JSON.parse(JSON.stringify(o));c.id=uid();c.page=page;delete c.key;delete c.orig;delete c.st;c.x=Math.min(Math.max(0,c.x+18),Math.max(0,W-c.w));c.y=Math.min(Math.max(0,c.y+18),Math.max(0,H-c.h));objs.push(c);sel=c.id;editing=null;render();toast('Object duplicated.')}
|
| 96 |
+
if(a==='X')del()};
|
| 97 |
+
ft.oninput=e=>{const a=e.target.dataset.a;if(a==='S'){const v=+e.target.value;if(v>=4)upd(o=>{o.size=v;o.h=v*1.2})}else if(a==='F')upd(o=>o.family=e.target.value);else if(a==='C')upd(o=>o.color=e.target.value)};
|
| 98 |
+
function del(){const o=cur();if(!o)return;endEdit();snap();
|
| 99 |
+
if(o.key&&o.type==='cover')objs=objs.filter(x=>x.key!==o.key);else objs=objs.filter(x=>x.id!==o.id);sel=null;render()}
|
| 100 |
+
function syncFT(){
|
| 101 |
+
const o=cur(),el=o&&ov&&ov.querySelector(`[data-id="${o.id}"]`);ft.style.display=o&&el?'flex':'none';if(!o||!el)return;
|
| 102 |
+
const t=o.type==='text',set=(a,f)=>{const n=fq(a);if(document.activeElement!==n)f(n)};
|
| 103 |
+
ft.querySelectorAll('.tx').forEach(n=>n.style.display=t?'':'none');
|
| 104 |
+
if(t&&isC(o.family)){fq('B').style.display='none';fq('I').style.display='none'}
|
| 105 |
+
set('C',n=>n.value=o.color);if(t){set('F',n=>n.value=o.family);set('S',n=>n.value=Math.round(o.size*2)/2);fq('B').classList.toggle('on',!!o.bold);fq('I').classList.toggle('on',!!o.italic)}
|
| 106 |
+
const top=el.offsetTop<56?el.offsetTop+el.offsetHeight+8:el.offsetTop-50;ft.style.left=Math.max(0,el.offsetLeft)+'px';ft.style.top=top+'px';
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* ---------- open / render ---------- */
|
| 110 |
+
$('openBtn').onclick=$('welcomeOpen').onclick=()=>$('fileInput').click();
|
| 111 |
+
$('fileInput').onchange=async e=>{const f=e.target.files[0];if(!f)return;try{bytes=new Uint8Array(await f.arrayBuffer());await load();objs=[];undo=[];sel=editing=null;page=1;$('welcome').hidden=true;$('undoBtn').disabled=true;await draw();status(f.name+' · '+pages+' page(s). '+MSG.text)}catch(x){console.error(x);toast('Could not open this PDF (it may be encrypted).')}e.target.value=''};
|
| 112 |
+
async function load(){doc=await pdfjsLib.getDocument({data:bytes.slice()}).promise;pages=doc.numPages}
|
| 113 |
+
async function draw(){
|
| 114 |
+
const my=++drawId,pg=await doc.getPage(page),vp=pg.getViewport({scale:zoom}),b=pg.getViewport({scale:1});W=b.width;H=b.height;
|
| 115 |
+
$('canvasArea').querySelectorAll('.page-wrap').forEach(n=>n.remove());
|
| 116 |
+
const wrap=document.createElement('div');wrap.className='page-wrap';wrap.style.width=vp.width+'px';wrap.style.height=vp.height+'px';
|
| 117 |
+
cv=document.createElement('canvas');
|
| 118 |
+
const dpr=Math.max(1,Math.min(2,window.devicePixelRatio||1));
|
| 119 |
+
cv.width=Math.ceil(vp.width*dpr);cv.height=Math.ceil(vp.height*dpr);
|
| 120 |
+
cv.style.width=vp.width+'px';cv.style.height=vp.height+'px';wrap.appendChild(cv);
|
| 121 |
+
tl=document.createElement('div');tl.className='tl';wrap.appendChild(tl);
|
| 122 |
+
ov=document.createElement('div');ov.onpointerdown=ovDown;wrap.appendChild(ov);wrap.appendChild(ft);$('canvasArea').appendChild(wrap);
|
| 123 |
+
await pg.render({canvasContext:cv.getContext('2d',{willReadFrequently:true}),viewport:vp,transform:dpr===1?null:[dpr,0,0,dpr,0,0]}).promise;
|
| 124 |
+
if(my!==drawId)return;curPg=pg;const tc=await pg.getTextContent();if(my!==drawId)return;
|
| 125 |
+
try{pdfjsLib.renderTextLayer({textContentSource:tc,container:tl,viewport:vp,textDivs:[]})}catch(e){console.warn('text layer',e)}
|
| 126 |
+
blocks=await getBlocks(pg,b,tc);render();ctl();
|
| 127 |
+
}
|
| 128 |
+
function ctl(){const l=!!doc;['zoomIn','zoomOut','fitBtn','saveBtn','delPage'].forEach(i=>$(i).disabled=!l);$('prevBtn').disabled=!l||page<=1;$('nextBtn').disabled=!l||page>=pages;$('pageLabel').textContent=l?`Page ${page} / ${pages}`:'No PDF open';$('zoomLabel').textContent=Math.round(zoom*100)+'%'}
|
| 129 |
+
|
| 130 |
+
/* ---------- read existing text: position, font, size, colour ---------- */
|
| 131 |
+
async function getBlocks(pg,b,tc){
|
| 132 |
+
const out=[];let last=null;
|
| 133 |
+
for(const it of tc.items){
|
| 134 |
+
if(!it.str||!it.transform)continue;const t=it.transform,size=Math.hypot(t[2],t[3]);if(!size)continue;
|
| 135 |
+
const [x,y]=b.convertToViewportPoint(t[4],t[5]);
|
| 136 |
+
if(last&&last.font===it.fontName&&Math.abs(last.y-y)<size*.3&&x>=last.x&&x-(last.x+last.w)<size*.8){
|
| 137 |
+
const gap=x-(last.x+last.w);if(gap>size*.2&&!/\s$/.test(last.text)&&!/^\s/.test(it.str))last.text+=' ';
|
| 138 |
+
last.text+=it.str;last.w=x+it.width-last.x;continue}
|
| 139 |
+
let name='';try{name=pg.commonObjs.get(it.fontName).name||''}catch(e){}
|
| 140 |
+
last={x,y,w:it.width,size,font:it.fontName,text:it.str,name,ff:(tc.styles[it.fontName]||{}).fontFamily||''};out.push(last);
|
| 141 |
+
}
|
| 142 |
+
return out.filter(o=>o.text.trim());
|
| 143 |
+
}
|
| 144 |
+
function fam(o){
|
| 145 |
+
const n=(o.name+' '+o.ff).toLowerCase().replace(/^[a-z]{6}\\+/,'');
|
| 146 |
+
if(/courier|mono|consolas|typewriter|lucida console/.test(n))return'mono';
|
| 147 |
+
if(/times|georgia|garamond|cambria|palatino|minion|bookman|roman|baskerville/.test(n)||o.ff==='serif')return'serif';
|
| 148 |
+
return 'sans';
|
| 149 |
+
}
|
| 150 |
+
function cssSourceFont(o){
|
| 151 |
+
const n=(o.name+' '+o.ff).toLowerCase();
|
| 152 |
+
if(/arial/.test(n))return 'Arial, Helvetica, sans-serif';
|
| 153 |
+
if(/helvetica/.test(n))return 'Helvetica, Arial, sans-serif';
|
| 154 |
+
if(/calibri/.test(n))return 'Calibri, Arial, sans-serif';
|
| 155 |
+
if(/aptos/.test(n))return 'Aptos, Calibri, Arial, sans-serif';
|
| 156 |
+
if(/verdana/.test(n))return 'Verdana, Arial, sans-serif';
|
| 157 |
+
if(/tahoma/.test(n))return 'Tahoma, Arial, sans-serif';
|
| 158 |
+
if(/times new roman|timesroman/.test(n))return '"Times New Roman", Times, serif';
|
| 159 |
+
if(/georgia/.test(n))return 'Georgia, serif';
|
| 160 |
+
if(/courier new|courier/.test(n))return '"Courier New", Courier, monospace';
|
| 161 |
+
return FF[fam(o)]||FF.sans;
|
| 162 |
+
}
|
| 163 |
+
function sample(x,y,w,h){
|
| 164 |
+
const X=Math.max(0,Math.floor(x*zoom)),Y=Math.max(0,Math.floor(y*zoom)),w2=Math.max(1,Math.min(cv.width-X,Math.ceil(w*zoom))),h2=Math.max(1,Math.min(cv.height-Y,Math.ceil(h*zoom)));
|
| 165 |
+
const d=cv.getContext('2d').getImageData(X,Y,w2,h2).data,cnt={};let bg=[255,255,255],bn=0;
|
| 166 |
+
for(let i=0;i<d.length;i+=4){const k=(d[i]>>4)<<8|(d[i+1]>>4)<<4|d[i+2]>>4,e=cnt[k]||(cnt[k]={n:0,c:[d[i],d[i+1],d[i+2]]});if(++e.n>bn){bn=e.n;bg=e.c}}
|
| 167 |
+
let fg=[0,0,0],md=-1;for(let i=0;i<d.length;i+=4){const dd=Math.abs(d[i]-bg[0])+Math.abs(d[i+1]-bg[1])+Math.abs(d[i+2]-bg[2]);if(dd>md){md=dd;fg=[d[i],d[i+1],d[i+2]]}}
|
| 168 |
+
const hx=a=>'#'+a.map(v=>v.toString(16).padStart(2,'0')).join('');return{bg:hx(bg),fg:md<60?'#000000':hx(fg)};
|
| 169 |
+
}
|
| 170 |
+
/* ---------- custom fonts: auto-matched by name ---------- */
|
| 171 |
+
const nrm=s=>s.replace(/^[A-Z]{6}\+/,'').toLowerCase().replace(/[^a-z0-9]/g,''),STY=/bold|italic|oblique|regular|black|heavy|demi|semi|light|medium/g;
|
| 172 |
+
const sty=n=>(/bold|black|heavy|demi/.test(n)?1:0)+(/italic|oblique/.test(n)?2:0);
|
| 173 |
+
function matchCustom(bk){const n=nrm(bk.name),b=n.replace(STY,'');if(b.length<3)return null;let best=null,bs=9;
|
| 174 |
+
for(const k in CF){const m=nrm(CF[k].name),mb=m.replace(STY,'');if(mb.length<3||!(b.includes(mb)||mb.includes(b)))continue;const d=Math.abs(sty(n)-sty(m));if(d<bs){bs=d;best=k}}return best}
|
| 175 |
+
function fontList(){$('fontNames').textContent='Fonts: '+(Object.values(CF).filter(f=>!f.emb).map(f=>f.name).join(', ')||'none');const s=fq('F');s.querySelectorAll('.cu').forEach(n=>n.remove());
|
| 176 |
+
for(const k in CF){const op=new Option(CF[k].label||CF[k].name,k);op.className='cu';s.add(op)}}
|
| 177 |
+
/* use the PDF's own embedded font program (exact look) when the browser/fontkit can read it */
|
| 178 |
+
const BAD=new Set();
|
| 179 |
+
function embFor(bk){const k='c:emb-'+bk.font;if(CF[k])return k;if(BAD.has(k))return null;
|
| 180 |
+
try{const fo=curPg.commonObjs.get(bk.font),d=fo&&fo.data;if(!d||!d.length)throw 0;
|
| 181 |
+
const u8=new Uint8Array(d),fk=fontkit.create(u8),a=fk.ascent/fk.unitsPerEm,dd=Math.abs(fk.descent)/fk.unitsPerEm;
|
| 182 |
+
CF[k]={name:fo.loadedName,label:'PDF font: '+(bk.name||'embedded').replace(/^[A-Z]{6}\+/,''),bytes:u8,bl:(1.2-(a+dd))/2+a,emb:1};fontList();return k}
|
| 183 |
+
catch(e){BAD.add(k);return null}}
|
| 184 |
+
$('fontBtn').onclick=()=>$('fontInput').click();
|
| 185 |
+
$('fontInput').onchange=async e=>{
|
| 186 |
+
for(const f of e.target.files){try{
|
| 187 |
+
const buf=await f.arrayBuffer(),name=f.name.replace(/\.[^.]+$/,'').replace(/["']/g,''),ff=new FontFace(name,buf.slice(0));await ff.load();document.fonts.add(ff);
|
| 188 |
+
let bl=ASC;try{const fk=fontkit.create(new Uint8Array(buf)),a=fk.ascent/fk.unitsPerEm,d=Math.abs(fk.descent)/fk.unitsPerEm;bl=(1.2-(a+d))/2+a}catch(x){}
|
| 189 |
+
CF['c:'+name]={name,bytes:new Uint8Array(buf),bl};
|
| 190 |
+
}catch(x){console.error(x);toast('Could not load font '+f.name)}}
|
| 191 |
+
e.target.value='';fontList();toast('Fonts loaded. Upload the PDF’s own fonts (regular/bold/italic files) and they match automatically.')};
|
| 192 |
+
|
| 193 |
+
/* ---------- editing existing text ---------- */
|
| 194 |
+
function editBlock(bk){
|
| 195 |
+
endEdit();const key=page+':'+Math.round(bk.x)+':'+Math.round(bk.y);let t=objs.find(o=>o.key===key&&o.type==='text');snap();
|
| 196 |
+
if(!t){const top=bk.y-ASC*bk.size,h=bk.size*1.2,cl=sample(bk.x,top,bk.w,h),um=matchCustom(bk),cm=um||embFor(bk),family=cm||fam(bk);
|
| 197 |
+
if(!objs.find(o=>o.key===key&&o.type==='cover'))objs.push({id:uid(),page,type:'cover',key,x:bk.x-1,y:top-.5,w:bk.w+3,h:h+1,color:cl.bg});
|
| 198 |
+
t={id:uid(),page,type:'text',key,x:bk.x,y:bk.y-BL(family)*bk.size,h,text:bk.text,orig:bk.text,size:+bk.size.toFixed(1),family,sourceCss:cssSourceFont(bk),fb:fam(bk),bold:/bold|black|heavy|demi/i.test(bk.name),italic:/italic|oblique/i.test(bk.name),color:cl.fg};
|
| 199 |
+
t.st=stk(t);objs.push(t);if(um)toast('Matched your font: '+CF[um].name)}
|
| 200 |
+
sel=editing=t.id;render();
|
| 201 |
+
}
|
| 202 |
+
function endEdit(){const o=objs.find(x=>x.id===editing);if(o)finish(o,true)}
|
| 203 |
+
function finish(o,quiet){
|
| 204 |
+
if(editing!==o.id)return;const el=ov.querySelector('.editing'),v=el?el.innerText.replace(/\n$/,''):(o.draft??o.text);editing=null;delete o.draft;
|
| 205 |
+
if(o.orig!==undefined&&v===o.orig&&stk(o)===o.st)objs=objs.filter(x=>x.key!==o.key); // unchanged → restore original
|
| 206 |
+
else if(!v.trim())objs=objs.filter(x=>x.id!==o.id); // emptied → text deleted
|
| 207 |
+
else o.text=v;
|
| 208 |
+
if(!objs.includes(o))sel=null;if(!quiet)render();
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/* ---------- overlay ---------- */
|
| 212 |
+
function render(){
|
| 213 |
+
if(!ov)return;const old=ov.querySelector('.editing');
|
| 214 |
+
if(old){const o=objs.find(x=>x.id===editing);if(o)o.draft=old.innerText.replace(/\n$/,'');old.onblur=null}
|
| 215 |
+
ov.innerHTML='';ov.className='overlay '+(mode==='text'?'t':mode==='select'?'s':'c');if(tl)tl.classList.toggle('on',mode==='select');
|
| 216 |
+
if(mode==='text')for(const bk of blocks){
|
| 217 |
+
const d=document.createElement('div');d.className='blk';d.title='Click to edit · drag in any direction to move';
|
| 218 |
+
d.style.cssText=`left:${bk.x*zoom}px;top:${(bk.y-ASC*bk.size)*zoom}px;width:${bk.w*zoom}px;height:${bk.size*1.2*zoom}px;touch-action:none`;
|
| 219 |
+
d.onpointerdown=e=>{
|
| 220 |
+
if(e.button!==0)return;
|
| 221 |
+
e.stopPropagation();e.preventDefault();
|
| 222 |
+
const sx=e.clientX,sy=e.clientY;
|
| 223 |
+
let moved=false,created=null,baseX=0,baseY=0;
|
| 224 |
+
const mv=ev=>{
|
| 225 |
+
const dx=ev.clientX-sx,dy=ev.clientY-sy;
|
| 226 |
+
if(!moved&&Math.hypot(dx,dy)>4){
|
| 227 |
+
moved=true;
|
| 228 |
+
endEdit();
|
| 229 |
+
const key=page+':'+Math.round(bk.x)+':'+Math.round(bk.y);
|
| 230 |
+
created=objs.find(o=>o.key===key&&o.type==='text');
|
| 231 |
+
if(!created){
|
| 232 |
+
editBlock(bk);
|
| 233 |
+
created=objs.find(o=>o.key===key&&o.type==='text');
|
| 234 |
+
editing=null;
|
| 235 |
+
}
|
| 236 |
+
if(created){
|
| 237 |
+
baseX=created.x;baseY=created.y;
|
| 238 |
+
snap();sel=created.id;
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
if(moved&&created){
|
| 242 |
+
created.x=Math.max(0,Math.min(W-2,baseX+dx/zoom));
|
| 243 |
+
created.y=Math.max(0,Math.min(H-created.size,baseY+dy/zoom));
|
| 244 |
+
const item=ov.querySelector('.item[data-id="'+created.id+'"]');
|
| 245 |
+
if(item){item.style.left=created.x*zoom+'px';item.style.top=created.y*zoom+'px'}
|
| 246 |
+
syncFT();
|
| 247 |
+
}
|
| 248 |
+
};
|
| 249 |
+
const up=()=>{
|
| 250 |
+
removeEventListener('pointermove',mv);
|
| 251 |
+
removeEventListener('pointerup',up);
|
| 252 |
+
removeEventListener('pointercancel',up);
|
| 253 |
+
if(moved&&created){render();toast('Text moved.')}
|
| 254 |
+
else editBlock(bk);
|
| 255 |
+
};
|
| 256 |
+
addEventListener('pointermove',mv);
|
| 257 |
+
addEventListener('pointerup',up);
|
| 258 |
+
addEventListener('pointercancel',up);
|
| 259 |
+
};
|
| 260 |
+
ov.appendChild(d);
|
| 261 |
+
}
|
| 262 |
+
for(const o of objs.filter(o=>o.page===page)){
|
| 263 |
+
const el=document.createElement('div'),s=el.style;el.className='item '+o.type+(sel===o.id?' sel':'');el.dataset.id=o.id;s.left=o.x*zoom+'px';s.top=o.y*zoom+'px';
|
| 264 |
+
if(o.type==='text'){
|
| 265 |
+
const cu=isC(o.family);s.fontSize=o.size*zoom+'px';s.lineHeight=o.size*1.2*zoom+'px';s.color=o.color;s.fontFamily=(o.sourceCss&&!cu&&!CF[o.family])?o.sourceCss:fcss(o);s.fontWeight=!cu&&o.bold?700:400;s.fontStyle=!cu&&o.italic?'italic':'normal';
|
| 266 |
+
el.textContent=editing===o.id&&o.draft!==undefined?o.draft:o.text;
|
| 267 |
+
if(editing===o.id){el.contentEditable='true';el.classList.add('editing');
|
| 268 |
+
setTimeout(()=>{el.focus();const r=document.createRange();r.selectNodeContents(el);const g=getSelection();g.removeAllRanges();g.addRange(r)});
|
| 269 |
+
el.onblur=()=>finish(o);el.onpaste=e=>{e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'))};
|
| 270 |
+
el.onkeydown=e=>{if(e.key==='Escape'){delete o.draft;el.textContent=o.text;el.blur()}else if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();el.blur()}}}
|
| 271 |
+
}else{
|
| 272 |
+
s.width=o.w*zoom+'px';s.height=o.h*zoom+'px';
|
| 273 |
+
if(o.type==='cover'||o.type==='highlight')s.background=o.color;else if(o.type==='rect')s.borderColor=o.color;
|
| 274 |
+
else if(o.type==='image'){const im=document.createElement('img');im.src=o.src;el.appendChild(im)}
|
| 275 |
+
else if(o.type==='draw')el.innerHTML=`<svg width="${o.w*zoom+4}" height="${o.h*zoom+4}" style="overflow:visible"><polyline fill="none" stroke="${o.color}" stroke-width="${2*zoom}" stroke-linecap="round" stroke-linejoin="round" points="${o.pts.map(p=>p.x*zoom+','+p.y*zoom).join(' ')}"/></svg>`;
|
| 276 |
+
if(sel===o.id&&o.type!=='draw'&&mode==='text'){const h=document.createElement('div');h.className='hd';h.onpointerdown=e=>{e.stopPropagation();snap();const sx=e.clientX,sy=e.clientY,w0=o.w,h0=o.h;
|
| 277 |
+
const mv=ev=>{o.w=Math.max(5,w0+(ev.clientX-sx)/zoom);o.h=Math.max(5,h0+(ev.clientY-sy)/zoom);s.width=o.w*zoom+'px';s.height=o.h*zoom+'px'};
|
| 278 |
+
const up=()=>{removeEventListener('pointermove',mv);removeEventListener('pointerup',up)};addEventListener('pointermove',mv);addEventListener('pointerup',up)};el.appendChild(h)}
|
| 279 |
+
}
|
| 280 |
+
el.onpointerdown=e=>{
|
| 281 |
+
if(mode!=='text'||editing===o.id)return;
|
| 282 |
+
if(e.target.classList.contains('hd')) return;
|
| 283 |
+
e.preventDefault(); e.stopPropagation();
|
| 284 |
+
endEdit(); sel=o.id;
|
| 285 |
+
const sx=e.clientX,sy=e.clientY,ox=o.x,oy=o.y;
|
| 286 |
+
let moved=false;
|
| 287 |
+
const mv=ev=>{
|
| 288 |
+
const dx=ev.clientX-sx,dy=ev.clientY-sy;
|
| 289 |
+
if(!moved&&Math.hypot(dx,dy)>3){moved=true;snap();el.classList.add('sel')}
|
| 290 |
+
if(moved){
|
| 291 |
+
o.x=Math.max(0,ox+dx/zoom);o.y=Math.max(0,oy+dy/zoom);
|
| 292 |
+
s.left=o.x*zoom+'px';s.top=o.y*zoom+'px';
|
| 293 |
+
syncFT();
|
| 294 |
+
}
|
| 295 |
+
};
|
| 296 |
+
const up=()=>{
|
| 297 |
+
removeEventListener('pointermove',mv);
|
| 298 |
+
removeEventListener('pointerup',up);
|
| 299 |
+
removeEventListener('pointercancel',up);
|
| 300 |
+
if(!moved&&o.type==='text'){snap();editing=o.id}
|
| 301 |
+
render();
|
| 302 |
+
};
|
| 303 |
+
addEventListener('pointermove',mv);
|
| 304 |
+
addEventListener('pointerup',up);
|
| 305 |
+
addEventListener('pointercancel',up);
|
| 306 |
+
};
|
| 307 |
+
ov.appendChild(el);
|
| 308 |
+
}
|
| 309 |
+
syncFT();
|
| 310 |
+
}
|
| 311 |
+
function add(o){snap();o.id=uid();o.page=page;objs.push(o);sel=o.id;$('saveBtn').disabled=false;if(mode!=='draw')setMode('text');else render();return o}
|
| 312 |
+
function ovDown(e){
|
| 313 |
+
if(!doc)return;const p=xy(e);
|
| 314 |
+
if(mode==='text'){
|
| 315 |
+
if(e.target!==ov)return;
|
| 316 |
+
if(editing||sel){endEdit();sel=null;render();return}
|
| 317 |
+
snap();const o={id:uid(),page,type:'text',x:p.x,y:p.y,h:16.8,text:'',size:14,family:'sans',bold:false,italic:false,color:'#000000'};objs.push(o);sel=editing=o.id;render();return}
|
| 318 |
+
if(!COL[mode])return;
|
| 319 |
+
const g=document.createElement('div');g.className='ghost';ov.appendChild(g);const pts=[p],md=mode;
|
| 320 |
+
const mv=ev=>{const q=xy(ev);pts.push(q);Object.assign(g.style,{left:Math.min(p.x,q.x)*zoom+'px',top:Math.min(p.y,q.y)*zoom+'px',width:Math.abs(q.x-p.x)*zoom+'px',height:Math.abs(q.y-p.y)*zoom+'px'});if(md==='draw'){g.style.border='0';g.style.background='transparent'}};
|
| 321 |
+
const up=ev=>{removeEventListener('pointermove',mv);removeEventListener('pointerup',up);g.remove();const q=xy(ev);
|
| 322 |
+
if(md==='draw'){if(pts.length<3)return;const x=Math.min(...pts.map(a=>a.x)),y=Math.min(...pts.map(a=>a.y));add({type:'draw',x,y,w:Math.max(...pts.map(a=>a.x))-x,h:Math.max(...pts.map(a=>a.y))-y,pts:pts.map(a=>({x:a.x-x,y:a.y-y})),color:COL.draw})}
|
| 323 |
+
else{const w=Math.abs(q.x-p.x),h=Math.abs(q.y-p.y);if(w>3&&h>3)add({type:md,x:Math.min(p.x,q.x),y:Math.min(p.y,q.y),w,h,color:COL[md]})}};
|
| 324 |
+
addEventListener('pointermove',mv);addEventListener('pointerup',up);
|
| 325 |
+
}
|
| 326 |
+
/* ---------- toolbar ---------- */
|
| 327 |
+
function setMode(m){endEdit();if(m===mode&&m!=='text')m='text';mode=m;if(m!=='text')sel=null;
|
| 328 |
+
document.querySelectorAll('#tb [data-mode]').forEach(b=>b.classList.toggle('active',b.dataset.mode===m));status(MSG[m]);if(m==='image'&&doc)$('imageInput').click();render()}
|
| 329 |
+
document.querySelectorAll('#tb [data-mode]').forEach(b=>b.onclick=()=>setMode(b.dataset.mode));
|
| 330 |
+
$('imageInput').onchange=e=>{const f=e.target.files[0];if(!f)return;const r=new FileReader();r.onload=()=>{const im=new Image();im.onload=()=>{const k=Math.min(1,220/im.width);add({type:'image',x:40,y:40,w:im.width*k,h:im.height*k,src:r.result})};im.src=r.result};r.readAsDataURL(f);e.target.value=''};
|
| 331 |
+
$('undoBtn').onclick=()=>{const s=undo.pop();if(s){objs=JSON.parse(s);sel=editing=null;render()}$('undoBtn').disabled=!undo.length};
|
| 332 |
+
addEventListener('keydown',e=>{
|
| 333 |
+
if(/INPUT|SELECT|TEXTAREA/.test(document.activeElement.tagName)||editing)return;
|
| 334 |
+
if((e.key==='Delete'||e.key==='Backspace')&&sel){e.preventDefault();del()}
|
| 335 |
+
if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='z'){e.preventDefault();$('undoBtn').click()}
|
| 336 |
+
});
|
| 337 |
+
const go=n=>{if(n<1||n>pages)return;endEdit();page=n;sel=editing=null;draw()};
|
| 338 |
+
$('prevBtn').onclick=()=>go(page-1);$('nextBtn').onclick=()=>go(page+1);
|
| 339 |
+
$('zoomIn').onclick=()=>{endEdit();zoom=Math.min(3,zoom+.15);draw()};$('zoomOut').onclick=()=>{endEdit();zoom=Math.max(.5,zoom-.15);draw()};
|
| 340 |
+
$('fitBtn').onclick=()=>{endEdit();zoom=Math.max(.5,Math.min(3,($('canvasArea').clientWidth-40)/W));draw()};
|
| 341 |
+
$('delPage').onclick=async()=>{if(pages<2)return toast('A PDF needs at least one page.');if(!confirm('Delete page '+page+'?'))return;
|
| 342 |
+
const d=await PDFDocument.load(bytes);d.removePage(page-1);bytes=await d.save();objs=objs.filter(o=>o.page!==page);objs.forEach(o=>{if(o.page>page)o.page--});
|
| 343 |
+
await load();page=Math.min(page,pages);undo=[];$('undoBtn').disabled=true;sel=editing=null;draw()};
|
| 344 |
+
|
| 345 |
+
/* ---------- export ---------- */
|
| 346 |
+
const hexC=h=>{const n=parseInt(h.slice(1),16);return rgb((n>>16&255)/255,(n>>8&255)/255,(n&255)/255)};
|
| 347 |
+
$('saveBtn').onclick=async()=>{try{
|
| 348 |
+
endEdit();status('Preparing PDF…');const out=await PDFDocument.load(bytes),fc={},csC=new Map();let warn=false;out.registerFontkit(fontkit);
|
| 349 |
+
const getF=async o=>{if(isC(o.family)&&CF[o.family]){const k=o.family;return fc[k]||(fc[k]=await out.embedFont(CF[k].bytes,{subset:true}))}
|
| 350 |
+
const k=FN[FN[o.family]?o.family:'sans'][(o.bold?1:0)+(o.italic?2:0)];return fc[k]||(fc[k]=await out.embedFont(StandardFonts[k]))};
|
| 351 |
+
for(const o of objs){
|
| 352 |
+
if(o.page<1||o.page>out.getPageCount())continue;
|
| 353 |
+
const p=out.getPage(o.page-1),{width:pw,height:ph}=p.getSize(),sx=pw/W,sy=ph/H;
|
| 354 |
+
if(o.type==='text'){
|
| 355 |
+
const csOf=f=>{let c=csC.get(f);if(!c){c=new Set(f.getCharacterSet());csC.set(f,c)}return c};
|
| 356 |
+
const fbO={family:FN[o.fb]?o.fb:'sans',bold:o.bold,italic:o.italic};let f,fb=null;
|
| 357 |
+
try{f=await getF(o);if(isC(o.family)&&CF[o.family])fb=await getF(fbO)}catch(err){console.warn('font embed failed',err);f=await getF(fbO);fb=null}
|
| 358 |
+
const cs=csOf(f),fcs=fb&&csOf(fb),sz=o.size*sy;
|
| 359 |
+
String(o.text).split('\n').forEach((ln,i)=>{
|
| 360 |
+
let x=o.x*sx;const y=ph-(o.y+BL(o.family)*o.size+i*o.size*1.2)*sy,runs=[];
|
| 361 |
+
for(const ch of ln){const cp=ch.codePointAt(0);let uf=f,c=ch;
|
| 362 |
+
if(!cs.has(cp)){if(fb&&fcs.has(cp))uf=fb;else{c='?';warn=true;uf=fb||f}}
|
| 363 |
+
const last=runs[runs.length-1];if(last&&last.f===uf)last.t+=c;else runs.push({f:uf,t:c})}
|
| 364 |
+
for(const r of runs){p.drawText(r.t,{x,y,size:sz,font:r.f,color:hexC(o.color)});x+=r.f.widthOfTextAtSize(r.t,sz)}});
|
| 365 |
+
continue}
|
| 366 |
+
const x=o.x*sx,y=ph-(o.y+o.h)*sy,w=o.w*sx,h=o.h*sy;
|
| 367 |
+
if(o.type==='cover')p.drawRectangle({x,y,width:w,height:h,color:hexC(o.color),borderWidth:0});
|
| 368 |
+
else if(o.type==='highlight')p.drawRectangle({x,y,width:w,height:h,color:hexC(o.color),opacity:.45,borderWidth:0});
|
| 369 |
+
else if(o.type==='rect')p.drawRectangle({x,y,width:w,height:h,borderColor:hexC(o.color),borderWidth:2});
|
| 370 |
+
else if(o.type==='image'){const d=Uint8Array.from(atob(o.src.split(',')[1]),c=>c.charCodeAt(0)),im=o.src.startsWith('data:image/png')?await out.embedPng(d):await out.embedJpg(d);p.drawImage(im,{x,y,width:w,height:h})}
|
| 371 |
+
else if(o.type==='draw')for(let j=1;j<o.pts.length;j++)p.drawLine({start:{x:(o.x+o.pts[j-1].x)*sx,y:ph-(o.y+o.pts[j-1].y)*sy},end:{x:(o.x+o.pts[j].x)*sx,y:ph-(o.y+o.pts[j].y)*sy},thickness:2,color:hexC(o.color)});
|
| 372 |
+
}
|
| 373 |
+
const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([await out.save()],{type:'application/pdf'}));a.download='PDFix-edited.pdf';a.click();
|
| 374 |
+
status('Exported.');toast(warn?'Exported. Some characters are not supported by the font and became “?”.':'Edited PDF exported.');
|
| 375 |
+
}catch(e){console.error(e);toast('Export failed.');status('Export failed.')}};
|
| 376 |
+
</script>
|
| 377 |
+
</body>
|
| 378 |
+
</html>
|