Talha912 commited on
Commit
2dfc24f
·
verified ·
1 Parent(s): c301d9c

Update edit.html

Browse files
Files changed (1) hide show
  1. edit.html +148 -163
edit.html CHANGED
@@ -3,89 +3,63 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>PDFix Advanced 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{--bg:#f4f7fb;--ink:#172033;--muted:#667085;--line:#e4e9f1;--blue:#2864e8;--soft:#edf3ff;--red:#c93434}
12
- *{box-sizing:border-box}body{margin:0;font-family:Inter,Segoe UI,Arial,sans-serif;color:var(--ink);background:var(--bg)}
13
- button,input,select{font:inherit}button{cursor:pointer;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:9px;padding:8px 12px;font-weight:600}button:hover{border-color:#9ab5f7}button.primary{background:var(--blue);color:#fff;border-color:var(--blue)}button.danger{color:var(--red)}button:disabled{opacity:.45;cursor:not-allowed}
14
- header{height:60px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:5}
15
- .brand{display:flex;align-items:center;gap:10px;font-size:20px;font-weight:800}.logo{width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,#2864e8,#7c4dff);display:grid;place-items:center;color:#fff}
16
- .hb{display:flex;gap:8px}
17
- .app{display:grid;grid-template-columns:200px minmax(300px,1fr) 240px;min-height:calc(100vh - 60px)}
18
- .side{background:#fff;padding:16px;border-right:1px solid var(--line)}.props{background:#fff;padding:16px;border-left:1px solid var(--line)}
19
- .pt{font-size:11px;font-weight:800;letter-spacing:1.2px;color:var(--muted);text-transform:uppercase;margin:2px 0 12px}
20
- .tg{display:grid;grid-template-columns:1fr 1fr;gap:8px}.tool{padding:10px 4px;text-align:center;font-size:12px;line-height:1.3}.tool i{display:block;font-style:normal;font-size:19px;margin-bottom:3px}.tool.active{background:var(--soft);border-color:#aac1ff;color:var(--blue)}
21
- .field{margin:12px 0}.field label{display:block;font-size:12px;font-weight:700;color:#475467;margin-bottom:5px}.field input:not([type=color]):not([type=checkbox]),.field select{width:100%;border:1px solid var(--line);border-radius:8px;padding:8px}.field input[type=color]{width:100%;height:36px;border:1px solid var(--line);border-radius:8px;padding:2px}
22
- .row{display:flex;gap:14px}.row label{display:flex;gap:6px;align-items:center}
23
- .hint,.empty{font-size:12px;line-height:1.5;color:var(--muted)}.empty{background:#f7f9fc;padding:14px;border-radius:10px}
24
- .ws{min-width:0;display:flex;flex-direction:column}.bar{display:flex;flex-wrap:wrap;align-items:center;gap:7px;padding:10px 14px;background:#fff;border-bottom:1px solid var(--line)}.bar .sp{flex:1}.bar button{padding:6px 10px;font-size:12px}
25
- .status{font-size:12px;color:var(--muted);padding:8px 14px;border-bottom:1px solid var(--line);background:#fbfcfe}
26
- .cv{flex:1;overflow:auto;padding:26px 16px 70px}
27
- .welcome{max-width:520px;margin:6vh auto;text-align:center;background:#fff;border:1px solid var(--line);border-radius:20px;padding:40px 30px}.welcome h1{font-size:25px}.welcome p{color:var(--muted);line-height:1.6}
28
- .page-wrap{position:relative;margin:0 auto;background:#fff;box-shadow:0 8px 30px rgba(22,34,61,.18)}.page-wrap canvas{display:block}
29
- .overlay{position:absolute;inset:0;overflow:hidden;touch-action:none}
30
- .blk{position:absolute;border:1px dashed rgba(40,100,232,.55);background:rgba(40,100,232,.06);cursor:text}.blk:hover{background:rgba(40,100,232,.22)}
31
  .item{position:absolute;cursor:move}.item.sel{outline:1px dashed var(--blue);outline-offset:2px}
32
- .item.text{white-space:pre;min-width:8px}.item.editing{outline:2px solid var(--blue);background:rgba(255,255,255,.92);cursor:text;min-width:20px}
33
  .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}
34
  .hd{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;background:var(--blue);cursor:nwse-resize;border-radius:3px}
35
- #fileInput,#imageInput{display:none}.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:20;display:none;max-width:90vw}
36
- @media(max-width:900px){.app{grid-template-columns:1fr}.tg{grid-template-columns:repeat(4,1fr)}}
 
 
37
  </style>
38
  </head>
39
  <body>
40
  <header>
41
- <div class="brand"><div class="logo">P</div>PDFix</div>
42
- <div class="hb"><button id="openBtn">Open PDF</button><button id="undoBtn" disabled>Undo</button><button class="primary" id="saveBtn" disabled>Export PDF</button></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </header>
44
- <input id="fileInput" type="file" accept="application/pdf,.pdf"><input id="imageInput" type="file" accept="image/png,image/jpeg">
45
- <div class="app">
46
- <aside class="side">
47
- <div class="pt">Tools</div>
48
- <div class="tg">
49
- <button class="tool active" data-mode="select"><i>↖</i>Select</button>
50
- <button class="tool" data-mode="edit"><i>✎</i>Edit text</button>
51
- <button class="tool" data-mode="text"><i>T</i>Add text</button>
52
- <button class="tool" data-mode="image"><i>▧</i>Image</button>
53
- <button class="tool" data-mode="highlight"><i>🖍</i>Highlight</button>
54
- <button class="tool" data-mode="draw"><i>〰</i>Draw</button>
55
- <button class="tool" data-mode="rect"><i>▱</i>Rectangle</button>
56
- <button class="tool" data-mode="cover"><i>□</i>Cover</button>
57
- </div>
58
- <div class="pt" style="margin-top:20px">Custom fonts</div>
59
- <button id="fontBtn" style="width:100%">Upload font (TTF/OTF)</button><input id="fontInput" type="file" accept=".ttf,.otf" multiple hidden>
60
- <p class="hint" id="fontNames">None yet</p>
61
- <p class="hint">Upload the PDF's own font files (e.g. Calibri.ttf, Calibri-Bold.ttf). They are matched automatically by name when you click text.</p>
62
- <div style="height:12px"></div>
63
- <button id="delPage" style="width:100%" class="danger" disabled>Delete this page</button>
64
- <p class="hint" style="margin-top:14px">Edit text: click any text → type → Enter to apply (Esc cancels, Shift+Enter = new line).</p>
65
- </aside>
66
- <main class="ws">
67
- <div class="bar">
68
- <button id="prevBtn" disabled>← Prev</button><button id="nextBtn" disabled>Next →</button><span id="pageLabel" class="hint">No PDF open</span>
69
- <span class="sp"></span>
70
- <button id="zoomOut" disabled>−</button><span id="zoomLabel" class="hint">125%</span><button id="zoomIn" disabled>+</button><button id="fitBtn" disabled>Fit width</button>
71
- </div>
72
- <div class="status" id="status">Open a PDF to begin. Your file stays in your browser.</div>
73
- <div class="cv" id="canvasArea">
74
- <div class="welcome" id="welcome"><div style="font-size:50px">📄</div><h1>Edit PDFs with PDFix</h1><p>Click text to edit it in the same font, size and colour. Add images, highlights, shapes; then export.</p><button class="primary" id="welcomeOpen">Choose a PDF</button></div>
75
- </div>
76
- </main>
77
- <aside class="props">
78
- <div class="pt">Properties</div>
79
- <div id="noSel" class="empty">Select an object, or use “Edit text” and click any text on the page.</div>
80
- <div id="pp" hidden>
81
- <div class="field tx"><label>Font</label><select id="pFam"><option value="sans">Helvetica / Arial (Sans)</option><option value="serif">Times (Serif)</option><option value="mono">Courier (Mono)</option></select></div>
82
- <div class="field tx"><label>Size (pt)</label><input id="pSize" type="number" min="4" max="200" step="0.5"></div>
83
- <div class="field"><label>Colour</label><input id="pCol" type="color"></div>
84
- <div class="field tx row"><label><input type="checkbox" id="pB"> Bold</label><label><input type="checkbox" id="pI"> Italic</label></div>
85
- <button id="delObj" class="danger" style="width:100%">Delete (restores original text)</button>
86
- </div>
87
- <p class="hint" style="margin-top:18px">Note: old text stays in the PDF under a cover. This is visual editing, not secure redaction.</p>
88
- </aside>
89
  </div>
90
  <div id="toast" class="toast"></div>
91
  <script>
@@ -93,56 +67,60 @@ pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/p
93
  const {PDFDocument,rgb,StandardFonts}=PDFLib,$=i=>document.getElementById(i);
94
  const FF={sans:'Arial,Helvetica,sans-serif',serif:'"Times New Roman",Times,serif',mono:'"Courier New",Courier,monospace'};
95
  const FN={sans:['Helvetica','HelveticaBold','HelveticaOblique','HelveticaBoldOblique'],serif:['TimesRoman','TimesRomanBold','TimesRomanItalic','TimesRomanBoldItalic'],mono:['Courier','CourierBold','CourierOblique','CourierBoldOblique']};
96
- const MSG={select:'Click an object to select/move it. Double-click text to edit.',edit:'Click any text on the page to edit it in place (same font, size, colour).',text:'Click on the page to type new text.',image:'Choose an image to insert.',highlight:'Drag to highlight.',draw:'Drag to draw.',rect:'Drag to draw a rectangle.',cover:'Drag to cover content with a coloured box.'};
97
- const ASC=.945; // baseline offset as a fraction of font size
98
- const CF={}; // custom fonts: 'c:Name' -> {name,bytes,bl}
99
- const isC=f=>f.startsWith('c:'),BL=f=>CF[f]?CF[f].bl:ASC;
 
100
  const fcss=o=>isC(o.family)&&CF[o.family]?`"${CF[o.family].name}",${FF.sans}`:FF[o.family]||FF.sans;
101
- const nrm=s=>s.replace(/^[A-Z]{6}\+/,'').toLowerCase().replace(/[^a-z0-9]/g,'');
102
- const STY=/bold|italic|oblique|regular|black|heavy|demi|semi|light|medium/g;
103
- const sty=n=>(/bold|black|heavy|demi/.test(n)?1:0)+(/italic|oblique/.test(n)?2:0);
104
- function matchCustom(bk){const n=nrm(bk.name),b=n.replace(STY,'');if(b.length<3)return null;let best=null,bs=9;
105
- 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}
106
- function fontList(){$('fontNames').textContent=Object.values(CF).map(f=>f.name).join(', ')||'None yet';const s=$('pFam');s.querySelectorAll('.cu').forEach(n=>n.remove());
107
- for(const k in CF){const op=new Option(CF[k].name+' (custom)',k);op.className='cu';s.add(op)}}
108
- $('fontBtn').onclick=()=>$('fontInput').click();
109
- $('fontInput').onchange=async e=>{
110
- for(const f of e.target.files){try{
111
- 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);
112
- 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){}
113
- CF['c:'+name]={name,bytes:new Uint8Array(buf),bl};
114
- }catch(x){console.error(x);toast('Could not load font '+f.name)}}
115
- e.target.value='';fontList();toast('Fonts loaded. Click text with “Edit text” to use them.')};
116
- let bytes,doc,page=1,pages=0,zoom=1.25,mode='select',sel=null,editing=null,objs=[],undo=[],W=0,H=0,cv,ov,blocks=[],drawId=0;
117
  const toast=m=>{const t=$('toast');t.textContent=m;t.style.display='block';clearTimeout(toast.t);toast.t=setTimeout(()=>t.style.display='none',3200)};
118
- const uid=()=>Math.random().toString(36).slice(2);
119
  const snap=()=>{undo.push(JSON.stringify(objs));if(undo.length>40)undo.shift();$('undoBtn').disabled=false};
120
- const cur=()=>objs.find(o=>o.id===sel);
121
  const xy=e=>{const r=ov.getBoundingClientRect();return{x:(e.clientX-r.left)/zoom,y:(e.clientY-r.top)/zoom}};
122
- const status=s=>$('status').textContent=s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  /* ---------- open / render ---------- */
125
  $('openBtn').onclick=$('welcomeOpen').onclick=()=>$('fileInput').click();
126
- $('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)')}catch(x){console.error(x);toast('Could not open this PDF (it may be encrypted).')}e.target.value=''};
127
  async function load(){doc=await pdfjsLib.getDocument({data:bytes.slice()}).promise;pages=doc.numPages}
128
  async function draw(){
129
  const my=++drawId,pg=await doc.getPage(page),vp=pg.getViewport({scale:zoom}),b=pg.getViewport({scale:1});W=b.width;H=b.height;
130
  $('canvasArea').querySelectorAll('.page-wrap').forEach(n=>n.remove());
131
  const wrap=document.createElement('div');wrap.className='page-wrap';wrap.style.width=vp.width+'px';wrap.style.height=vp.height+'px';
132
  cv=document.createElement('canvas');cv.width=Math.ceil(vp.width);cv.height=Math.ceil(vp.height);wrap.appendChild(cv);
133
- ov=document.createElement('div');ov.className='overlay';ov.onpointerdown=ovDown;wrap.appendChild(ov);$('canvasArea').appendChild(wrap);
134
  await pg.render({canvasContext:cv.getContext('2d',{willReadFrequently:true}),viewport:vp}).promise;
135
- if(my!==drawId)return;
136
- blocks=await getBlocks(pg,b);render();ctl();
137
  }
138
- 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} of ${pages}`:'No PDF open';$('zoomLabel').textContent=Math.round(zoom*100)+'%'}
139
 
140
- /* ---------- read existing text: position, font, size ---------- */
141
  async function getBlocks(pg,b){
142
  const tc=await pg.getTextContent(),out=[];let last=null;
143
  for(const it of tc.items){
144
- if(!it.str||!it.transform)continue;
145
- const t=it.transform,size=Math.hypot(t[2],t[3]);if(!size)continue;
146
  const [x,y]=b.convertToViewportPoint(t[4],t[5]);
147
  if(last&&last.font===it.fontName&&Math.abs(last.y-y)<size*.3&&x>=last.x&&x-(last.x+last.w)<size*.8){
148
  const gap=x-(last.x+last.w);if(gap>size*.2&&!/\s$/.test(last.text)&&!/^\s/.test(it.str))last.text+=' ';
@@ -154,7 +132,6 @@ async function getBlocks(pg,b){
154
  }
155
  function fam(o){const n=(o.name+' '+o.ff).toLowerCase();if(/courier|mono|consolas|typewriter/.test(n))return'mono';
156
  return(/times|georgia|garamond|cambria|palatino|minion|bookman|roman/.test(n)||o.ff==='serif')&&!/sans/.test(n)?'serif':'sans'}
157
- /* sample page pixels: most common colour = background, farthest from it = text colour */
158
  function sample(x,y,w,h){
159
  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)));
160
  const d=cv.getContext('2d').getImageData(X,Y,w2,h2).data,cnt={};let bg=[255,255,255],bn=0;
@@ -162,103 +139,111 @@ function sample(x,y,w,h){
162
  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]]}}
163
  const hx=a=>'#'+a.map(v=>v.toString(16).padStart(2,'0')).join('');return{bg:hx(bg),fg:md<60?'#000000':hx(fg)};
164
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  function editBlock(bk){
166
- const key=page+':'+Math.round(bk.x)+':'+Math.round(bk.y),ex=objs.find(o=>o.key===key&&o.type==='text');
167
- if(ex){snap();sel=editing=ex.id;render();syncProps();return}
168
- snap();const top=bk.y-ASC*bk.size,h=bk.size*1.2,cl=sample(bk.x,top,bk.w,h),cm=matchCustom(bk),family=cm||fam(bk);
169
- 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},
170
- {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,bold:!cm&&/bold|black|heavy|demi/i.test(bk.name),italic:!cm&&/italic|oblique/i.test(bk.name),color:cl.fg});
171
- if(cm)toast('Matched custom font: '+CF[cm].name);
172
- sel=editing=objs[objs.length-1].id;render();syncProps();
 
 
 
 
 
 
 
173
  }
174
 
175
- /* ---------- overlay rendering ---------- */
176
  function render(){
177
- if(!ov)return;ov.innerHTML='';
178
- if(mode==='edit')for(const bk of blocks){const d=document.createElement('div');d.className='blk';d.title=bk.text;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`;d.onpointerdown=e=>{e.stopPropagation();e.preventDefault();editBlock(bk)};ov.appendChild(d)}
 
 
179
  for(const o of objs.filter(o=>o.page===page)){
180
- const el=document.createElement('div'),s=el.style;el.className='item '+o.type+(sel===o.id?' sel':'');s.left=o.x*zoom+'px';s.top=o.y*zoom+'px';
181
  if(o.type==='text'){
182
- s.fontSize=o.size*zoom+'px';s.lineHeight=o.size*1.2*zoom+'px';s.color=o.color;const cu=isC(o.family);s.fontFamily=fcss(o);s.fontWeight=!cu&&o.bold?700:400;s.fontStyle=!cu&&o.italic?'italic':'normal';el.textContent=o.text;
 
183
  if(editing===o.id){el.contentEditable='true';el.classList.add('editing');
184
  setTimeout(()=>{el.focus();const r=document.createRange();r.selectNodeContents(el);const g=getSelection();g.removeAllRanges();g.addRange(r)});
185
  el.onblur=()=>finish(o);
186
- el.onkeydown=e=>{if(e.key==='Escape'){el.textContent=o.text;el.blur()}else if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();el.blur()}}}
187
  }else{
188
  s.width=o.w*zoom+'px';s.height=o.h*zoom+'px';
189
- if(o.type==='cover')s.background=o.color;
190
- else if(o.type==='highlight')s.background=o.color;
191
- else if(o.type==='rect')s.borderColor=o.color;
192
  else if(o.type==='image'){const im=document.createElement('img');im.src=o.src;el.appendChild(im)}
193
  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>`;
194
- if(sel===o.id&&o.type!=='draw'){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;
195
  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'};
196
  const up=()=>{removeEventListener('pointermove',mv);removeEventListener('pointerup',up)};addEventListener('pointermove',mv);addEventListener('pointerup',up)};el.appendChild(h)}
197
  }
198
  el.onpointerdown=e=>{
199
- if(mode==='edit'){if(o.type==='text'&&editing!==o.id){e.preventDefault();snap();sel=editing=o.id;render();syncProps()}return}
200
- if(mode!=='select'||editing===o.id)return;
201
- e.stopPropagation();sel=o.id;syncProps();snap();const sx=e.clientX,sy=e.clientY,ox=o.x,oy=o.y;
202
- const mv=ev=>{o.x=Math.max(0,ox+(ev.clientX-sx)/zoom);o.y=Math.max(0,oy+(ev.clientY-sy)/zoom);s.left=o.x*zoom+'px';s.top=o.y*zoom+'px'};
203
- const up=()=>{removeEventListener('pointermove',mv);removeEventListener('pointerup',up);render()};
204
- addEventListener('pointermove',mv);addEventListener('pointerup',up)};
205
- el.ondblclick=()=>{if(o.type==='text'&&mode==='select'){snap();sel=editing=o.id;render()}};
206
  ov.appendChild(el);
207
  }
 
208
  }
209
- function finish(o){
210
- if(editing!==o.id)return;const el=ov.querySelector('.editing'),v=el?el.innerText.replace(/\n$/,''):o.text;editing=null;
211
- if(o.orig!==undefined&&v===o.orig)objs=objs.filter(x=>x.key!==o.key); // unchanged → restore original
212
- else if(!v.trim())objs=objs.filter(x=>x.id!==o.id); // emptied → text removed (cover stays)
213
- else o.text=v;
214
- sel=null;render();syncProps();
215
- }
216
-
217
- /* ---------- tools ---------- */
218
- function setMode(m){mode=m;document.querySelectorAll('.tool').forEach(b=>b.classList.toggle('active',b.dataset.mode===m));status(MSG[m]);if(m==='image')$('imageInput').click();render()}
219
- document.querySelectorAll('.tool').forEach(b=>b.onclick=()=>setMode(b.dataset.mode));
220
- function add(o){snap();o.id=uid();o.page=page;objs.push(o);sel=o.id;$('saveBtn').disabled=false;render();syncProps();return o}
221
- const COL={highlight:'#ffeb3b',rect:'#2864e8',cover:'#ffffff',draw:'#2864e8'};
222
  function ovDown(e){
223
  if(!doc)return;const p=xy(e);
224
- if(mode==='select'||mode==='edit'){sel=null;render();syncProps();return}
225
- if(mode==='text'){const o=add({type:'text',x:p.x,y:p.y,h:16.8,text:'Text',size:14,family:'sans',bold:false,italic:false,color:'#000000'});editing=o.id;render();return}
 
 
226
  if(!COL[mode])return;
227
  const g=document.createElement('div');g.className='ghost';ov.appendChild(g);const pts=[p],md=mode;
228
  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'}};
229
  const up=ev=>{removeEventListener('pointermove',mv);removeEventListener('pointerup',up);g.remove();const q=xy(ev);
230
- if(md==='draw'){if(pts.length<2)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})}
231
  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]})}};
232
  addEventListener('pointermove',mv);addEventListener('pointerup',up);
233
  }
234
- $('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});setMode('select')};im.src=r.result};r.readAsDataURL(f);e.target.value=''};
235
-
236
- /* ---------- properties panel (live) ---------- */
237
- function syncProps(){const o=cur();$('noSel').hidden=!!o;$('pp').hidden=!o;if(!o)return;
238
- document.querySelectorAll('.tx').forEach(n=>n.hidden=o.type!=='text');
239
- $('pCol').value=o.color||'#000000';if(o.type==='text'){$('pFam').value=o.family;$('pSize').value=o.size;$('pB').checked=!!o.bold;$('pI').checked=!!o.italic}}
240
- const live=(id,fn)=>$(id).oninput=()=>{const o=cur();if(!o)return;fn(o);render()};
241
- live('pFam',o=>o.family=$('pFam').value);live('pSize',o=>{o.size=Math.max(4,+$('pSize').value||12);o.h=o.size*1.2});
242
- live('pCol',o=>o.color=$('pCol').value);live('pB',o=>o.bold=$('pB').checked);live('pI',o=>o.italic=$('pI').checked);
243
- $('delObj').onclick=()=>{const o=cur();if(!o)return;snap();objs=objs.filter(x=>o.key?x.key!==o.key:x.id!==o.id);sel=editing=null;render();syncProps()};
244
- $('undoBtn').onclick=()=>{const s=undo.pop();if(s){objs=JSON.parse(s);sel=editing=null;render();syncProps()}$('undoBtn').disabled=!undo.length};
245
  addEventListener('keydown',e=>{if(editing||/INPUT|SELECT/.test(document.activeElement.tagName))return;
246
- if((e.key==='Delete'||e.key==='Backspace')&&sel){e.preventDefault();$('delObj').click()}
247
  if((e.ctrlKey||e.metaKey)&&e.key==='z'){e.preventDefault();$('undoBtn').click()}});
248
-
249
- /* ---------- navigation ---------- */
250
- const go=n=>{if(n<1||n>pages)return;page=n;sel=editing=null;draw();syncProps()};
251
  $('prevBtn').onclick=()=>go(page-1);$('nextBtn').onclick=()=>go(page+1);
252
- $('zoomIn').onclick=()=>{zoom=Math.min(3,zoom+.15);draw()};$('zoomOut').onclick=()=>{zoom=Math.max(.5,zoom-.15);draw()};
253
- $('fitBtn').onclick=()=>{zoom=Math.max(.5,Math.min(3,($('canvasArea').clientWidth-50)/W));draw()};
254
  $('delPage').onclick=async()=>{if(pages<2)return toast('A PDF needs at least one page.');if(!confirm('Delete page '+page+'?'))return;
255
  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--});
256
  await load();page=Math.min(page,pages);undo=[];$('undoBtn').disabled=true;sel=editing=null;draw()};
257
 
258
- /* ---------- export: same font family/size/colour as shown ---------- */
259
  const hexC=h=>{const n=parseInt(h.slice(1),16);return rgb((n>>16&255)/255,(n>>8&255)/255,(n&255)/255)};
260
  $('saveBtn').onclick=async()=>{try{
261
- status('Preparing PDF…');const out=await PDFDocument.load(bytes),fc={},csC=new Map();let warn=false;out.registerFontkit(fontkit);
262
  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}))}
263
  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]))};
264
  for(const o of objs){
@@ -275,7 +260,7 @@ $('saveBtn').onclick=async()=>{try{
275
  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)});
276
  }
277
  const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([await out.save()],{type:'application/pdf'}));a.download='PDFix-edited.pdf';a.click();
278
- status('Exported.');toast(warn?'Exported. Some characters (non-Latin) are not supported by standard fonts and became “?”.':'Edited PDF exported.');
279
  }catch(e){console.error(e);toast('Export failed.');status('Export failed.')}};
280
  </script>
281
  </body>
 
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
+ .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}
34
  </style>
35
  </head>
36
  <body>
37
  <header>
38
+ <div class="top">
39
+ <div class="brand"><div class="logo">P</div>PDFix</div>
40
+ <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>
41
+ </div>
42
+ <div class="tb" id="tb">
43
+ <button data-mode="text" class="active">𝙸A Text</button>
44
+ <button data-mode="image">▧ Image</button>
45
+ <button data-mode="cover">◇ Whiteout</button>
46
+ <button data-mode="highlight">🖍 Highlight</button>
47
+ <button data-mode="draw">✎ Draw</button>
48
+ <button data-mode="rect">▱ Rectangle</button>
49
+ <button id="fontBtn">Aa Fonts</button>
50
+ <button id="undoBtn" disabled>⎌ Undo</button>
51
+ </div>
52
+ <div class="bar">
53
+ <button class="btn" id="prevBtn" disabled>←</button><span id="pageLabel">No PDF open</span><button class="btn" id="nextBtn" disabled>→</button>
54
+ <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>
55
+ <span>|</span><button class="btn danger" id="delPage" disabled>Delete page</button>
56
+ <span id="fontNames" title="Custom fonts loaded">Fonts: none</span>
57
+ </div>
58
  </header>
59
+ <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>
60
+ <div class="status" id="status">Open a PDF, then click any text to edit it. Click empty space to add new text.</div>
61
+ <div class="cv" id="canvasArea">
62
+ <div class="welcome" id="welcome"><div style="font-size:50px">📄</div><h1>Edit PDF online</h1><p>Click any text and edit it in the same font, size and colour. Add text, images, whiteout, highlights and shapes. Your file never leaves your browser.</p><button class="btn primary" id="welcomeOpen" style="padding:11px 22px;font-size:15px">Choose a PDF</button></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  </div>
64
  <div id="toast" class="toast"></div>
65
  <script>
 
67
  const {PDFDocument,rgb,StandardFonts}=PDFLib,$=i=>document.getElementById(i);
68
  const FF={sans:'Arial,Helvetica,sans-serif',serif:'"Times New Roman",Times,serif',mono:'"Courier New",Courier,monospace'};
69
  const FN={sans:['Helvetica','HelveticaBold','HelveticaOblique','HelveticaBoldOblique'],serif:['TimesRoman','TimesRomanBold','TimesRomanItalic','TimesRomanBoldItalic'],mono:['Courier','CourierBold','CourierOblique','CourierBoldOblique']};
70
+ const MSG={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.'};
71
+ const COL={highlight:'#ffeb3b',rect:'#2864e8',cover:'#ffffff',draw:'#2864e8'};
72
+ const ASC=.945,CF={};
73
+ let 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;
74
+ 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);
75
  const fcss=o=>isC(o.family)&&CF[o.family]?`"${CF[o.family].name}",${FF.sans}`:FF[o.family]||FF.sans;
76
+ const stk=o=>JSON.stringify([o.family,o.size,o.bold,o.italic,o.color]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  const toast=m=>{const t=$('toast');t.textContent=m;t.style.display='block';clearTimeout(toast.t);toast.t=setTimeout(()=>t.style.display='none',3200)};
78
+ const status=s=>$('status').textContent=s;
79
  const snap=()=>{undo.push(JSON.stringify(objs));if(undo.length>40)undo.shift();$('undoBtn').disabled=false};
 
80
  const xy=e=>{const r=ov.getBoundingClientRect();return{x:(e.clientX-r.left)/zoom,y:(e.clientY-r.top)/zoom}};
81
+
82
+ /* ---------- floating toolbar (like Sejda) ---------- */
83
+ const ft=document.createElement('div');ft.className='ft';
84
+ 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>`;
85
+ const fq=a=>ft.querySelector(`[data-a="${a}"]`);
86
+ ft.onpointerdown=e=>{if(!/INPUT|SELECT/.test(e.target.tagName))e.preventDefault()};
87
+ const upd=fn=>{const o=cur();if(!o)return;fn(o);render()};
88
+ ft.onclick=e=>{const a=e.target.closest('button')?.dataset.a;if(!a)return;
89
+ if(a==='B')upd(o=>o.bold=!o.bold);if(a==='I')upd(o=>o.italic=!o.italic);
90
+ 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});
91
+ if(a==='D'){const o=cur();if(!o)return;endEdit();snap();const c=JSON.parse(JSON.stringify(o));c.id=uid();delete c.key;delete c.orig;delete c.st;c.x+=14;c.y+=14;objs.push(c);sel=c.id;render()}
92
+ if(a==='X')del()};
93
+ 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)};
94
+ function del(){const o=cur();if(!o)return;endEdit();snap();
95
+ 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()}
96
+ function syncFT(){
97
+ const o=cur(),el=o&&ov&&ov.querySelector(`[data-id="${o.id}"]`);ft.style.display=o&&el?'flex':'none';if(!o||!el)return;
98
+ const t=o.type==='text',set=(a,f)=>{const n=fq(a);if(document.activeElement!==n)f(n)};
99
+ ft.querySelectorAll('.tx').forEach(n=>n.style.display=t?'':'none');
100
+ 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)}
101
+ 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';
102
+ }
103
 
104
  /* ---------- open / render ---------- */
105
  $('openBtn').onclick=$('welcomeOpen').onclick=()=>$('fileInput').click();
106
+ $('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=''};
107
  async function load(){doc=await pdfjsLib.getDocument({data:bytes.slice()}).promise;pages=doc.numPages}
108
  async function draw(){
109
  const my=++drawId,pg=await doc.getPage(page),vp=pg.getViewport({scale:zoom}),b=pg.getViewport({scale:1});W=b.width;H=b.height;
110
  $('canvasArea').querySelectorAll('.page-wrap').forEach(n=>n.remove());
111
  const wrap=document.createElement('div');wrap.className='page-wrap';wrap.style.width=vp.width+'px';wrap.style.height=vp.height+'px';
112
  cv=document.createElement('canvas');cv.width=Math.ceil(vp.width);cv.height=Math.ceil(vp.height);wrap.appendChild(cv);
113
+ ov=document.createElement('div');ov.onpointerdown=ovDown;wrap.appendChild(ov);wrap.appendChild(ft);$('canvasArea').appendChild(wrap);
114
  await pg.render({canvasContext:cv.getContext('2d',{willReadFrequently:true}),viewport:vp}).promise;
115
+ if(my!==drawId)return;blocks=await getBlocks(pg,b);render();ctl();
 
116
  }
117
+ 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)+'%'}
118
 
119
+ /* ---------- read existing text: position, font, size, colour ---------- */
120
  async function getBlocks(pg,b){
121
  const tc=await pg.getTextContent(),out=[];let last=null;
122
  for(const it of tc.items){
123
+ if(!it.str||!it.transform)continue;const t=it.transform,size=Math.hypot(t[2],t[3]);if(!size)continue;
 
124
  const [x,y]=b.convertToViewportPoint(t[4],t[5]);
125
  if(last&&last.font===it.fontName&&Math.abs(last.y-y)<size*.3&&x>=last.x&&x-(last.x+last.w)<size*.8){
126
  const gap=x-(last.x+last.w);if(gap>size*.2&&!/\s$/.test(last.text)&&!/^\s/.test(it.str))last.text+=' ';
 
132
  }
133
  function fam(o){const n=(o.name+' '+o.ff).toLowerCase();if(/courier|mono|consolas|typewriter/.test(n))return'mono';
134
  return(/times|georgia|garamond|cambria|palatino|minion|bookman|roman/.test(n)||o.ff==='serif')&&!/sans/.test(n)?'serif':'sans'}
 
135
  function sample(x,y,w,h){
136
  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)));
137
  const d=cv.getContext('2d').getImageData(X,Y,w2,h2).data,cnt={};let bg=[255,255,255],bn=0;
 
139
  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]]}}
140
  const hx=a=>'#'+a.map(v=>v.toString(16).padStart(2,'0')).join('');return{bg:hx(bg),fg:md<60?'#000000':hx(fg)};
141
  }
142
+ /* ---------- custom fonts: auto-matched by name ---------- */
143
+ 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;
144
+ const sty=n=>(/bold|black|heavy|demi/.test(n)?1:0)+(/italic|oblique/.test(n)?2:0);
145
+ function matchCustom(bk){const n=nrm(bk.name),b=n.replace(STY,'');if(b.length<3)return null;let best=null,bs=9;
146
+ 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}
147
+ function fontList(){$('fontNames').textContent='Fonts: '+(Object.values(CF).map(f=>f.name).join(', ')||'none');const s=fq('F');s.querySelectorAll('.cu').forEach(n=>n.remove());
148
+ for(const k in CF){const op=new Option(CF[k].name,k);op.className='cu';s.add(op)}}
149
+ $('fontBtn').onclick=()=>$('fontInput').click();
150
+ $('fontInput').onchange=async e=>{
151
+ for(const f of e.target.files){try{
152
+ 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);
153
+ 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){}
154
+ CF['c:'+name]={name,bytes:new Uint8Array(buf),bl};
155
+ }catch(x){console.error(x);toast('Could not load font '+f.name)}}
156
+ e.target.value='';fontList();toast('Fonts loaded. Upload the PDF’s own fonts (regular/bold/italic files) and they match automatically.')};
157
+
158
+ /* ---------- editing existing text ---------- */
159
  function editBlock(bk){
160
+ endEdit();const key=page+':'+Math.round(bk.x)+':'+Math.round(bk.y);let t=objs.find(o=>o.key===key&&o.type==='text');snap();
161
+ if(!t){const top=bk.y-ASC*bk.size,h=bk.size*1.2,cl=sample(bk.x,top,bk.w,h),cm=matchCustom(bk),family=cm||fam(bk);
162
+ 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});
163
+ 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,bold:!cm&&/bold|black|heavy|demi/i.test(bk.name),italic:!cm&&/italic|oblique/i.test(bk.name),color:cl.fg};
164
+ t.st=stk(t);objs.push(t);if(cm)toast('Matched your font: '+CF[cm].name)}
165
+ sel=editing=t.id;render();
166
+ }
167
+ function endEdit(){const o=objs.find(x=>x.id===editing);if(o)finish(o,true)}
168
+ function finish(o,quiet){
169
+ 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;
170
+ if(o.orig!==undefined&&v===o.orig&&stk(o)===o.st)objs=objs.filter(x=>x.key!==o.key); // unchanged → restore original
171
+ else if(!v.trim())objs=objs.filter(x=>x.id!==o.id); // emptied → text deleted
172
+ else o.text=v;
173
+ if(!objs.includes(o))sel=null;if(!quiet)render();
174
  }
175
 
176
+ /* ---------- overlay ---------- */
177
  function render(){
178
+ if(!ov)return;const old=ov.querySelector('.editing');
179
+ if(old){const o=objs.find(x=>x.id===editing);if(o)o.draft=old.innerText.replace(/\n$/,'');old.onblur=null}
180
+ ov.innerHTML='';ov.className='overlay '+(mode==='text'?'t':'c');
181
+ if(mode==='text')for(const bk of blocks){const d=document.createElement('div');d.className='blk';d.title='Click to edit';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`;d.onpointerdown=e=>{e.stopPropagation();e.preventDefault();editBlock(bk)};ov.appendChild(d)}
182
  for(const o of objs.filter(o=>o.page===page)){
183
+ 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';
184
  if(o.type==='text'){
185
+ 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=fcss(o);s.fontWeight=!cu&&o.bold?700:400;s.fontStyle=!cu&&o.italic?'italic':'normal';
186
+ el.textContent=editing===o.id&&o.draft!==undefined?o.draft:o.text;
187
  if(editing===o.id){el.contentEditable='true';el.classList.add('editing');
188
  setTimeout(()=>{el.focus();const r=document.createRange();r.selectNodeContents(el);const g=getSelection();g.removeAllRanges();g.addRange(r)});
189
  el.onblur=()=>finish(o);
190
+ 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()}}}
191
  }else{
192
  s.width=o.w*zoom+'px';s.height=o.h*zoom+'px';
193
+ if(o.type==='cover'||o.type==='highlight')s.background=o.color;else if(o.type==='rect')s.borderColor=o.color;
 
 
194
  else if(o.type==='image'){const im=document.createElement('img');im.src=o.src;el.appendChild(im)}
195
  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>`;
196
+ 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;
197
  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'};
198
  const up=()=>{removeEventListener('pointermove',mv);removeEventListener('pointerup',up)};addEventListener('pointermove',mv);addEventListener('pointerup',up)};el.appendChild(h)}
199
  }
200
  el.onpointerdown=e=>{
201
+ if(mode!=='text'||editing===o.id)return;e.stopPropagation();endEdit();sel=o.id;
202
+ const sx=e.clientX,sy=e.clientY,ox=o.x,oy=o.y;let moved=false;
203
+ const mv=ev=>{const dx=ev.clientX-sx,dy=ev.clientY-sy;if(!moved&&Math.hypot(dx,dy)>3){moved=true;snap()}if(moved){o.x=Math.max(0,ox+dx/zoom);o.y=Math.max(0,oy+dy/zoom);s.left=o.x*zoom+'px';s.top=o.y*zoom+'px';syncFT()}};
204
+ const up=()=>{removeEventListener('pointermove',mv);removeEventListener('pointerup',up);if(!moved&&o.type==='text'){snap();editing=o.id}render()};
205
+ addEventListener('pointermove',mv);addEventListener('pointerup',up);render();
206
+ };
 
207
  ov.appendChild(el);
208
  }
209
+ syncFT();
210
  }
211
+ 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}
 
 
 
 
 
 
 
 
 
 
 
 
212
  function ovDown(e){
213
  if(!doc)return;const p=xy(e);
214
+ if(mode==='text'){
215
+ if(e.target!==ov)return;
216
+ if(editing||sel){endEdit();sel=null;render();return}
217
+ 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}
218
  if(!COL[mode])return;
219
  const g=document.createElement('div');g.className='ghost';ov.appendChild(g);const pts=[p],md=mode;
220
  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'}};
221
  const up=ev=>{removeEventListener('pointermove',mv);removeEventListener('pointerup',up);g.remove();const q=xy(ev);
222
+ 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})}
223
  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]})}};
224
  addEventListener('pointermove',mv);addEventListener('pointerup',up);
225
  }
226
+ /* ---------- toolbar ---------- */
227
+ function setMode(m){endEdit();if(m===mode&&m!=='text')m='text';mode=m;if(m!=='text')sel=null;
228
+ 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()}
229
+ document.querySelectorAll('#tb [data-mode]').forEach(b=>b.onclick=()=>setMode(b.dataset.mode));
230
+ $('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=''};
231
+ $('undoBtn').onclick=()=>{const s=undo.pop();if(s){objs=JSON.parse(s);sel=editing=null;render()}$('undoBtn').disabled=!undo.length};
 
 
 
 
 
232
  addEventListener('keydown',e=>{if(editing||/INPUT|SELECT/.test(document.activeElement.tagName))return;
233
+ if((e.key==='Delete'||e.key==='Backspace')&&sel){e.preventDefault();del()}
234
  if((e.ctrlKey||e.metaKey)&&e.key==='z'){e.preventDefault();$('undoBtn').click()}});
235
+ const go=n=>{if(n<1||n>pages)return;endEdit();page=n;sel=editing=null;draw()};
 
 
236
  $('prevBtn').onclick=()=>go(page-1);$('nextBtn').onclick=()=>go(page+1);
237
+ $('zoomIn').onclick=()=>{endEdit();zoom=Math.min(3,zoom+.15);draw()};$('zoomOut').onclick=()=>{endEdit();zoom=Math.max(.5,zoom-.15);draw()};
238
+ $('fitBtn').onclick=()=>{endEdit();zoom=Math.max(.5,Math.min(3,($('canvasArea').clientWidth-40)/W));draw()};
239
  $('delPage').onclick=async()=>{if(pages<2)return toast('A PDF needs at least one page.');if(!confirm('Delete page '+page+'?'))return;
240
  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--});
241
  await load();page=Math.min(page,pages);undo=[];$('undoBtn').disabled=true;sel=editing=null;draw()};
242
 
243
+ /* ---------- export ---------- */
244
  const hexC=h=>{const n=parseInt(h.slice(1),16);return rgb((n>>16&255)/255,(n>>8&255)/255,(n&255)/255)};
245
  $('saveBtn').onclick=async()=>{try{
246
+ endEdit();status('Preparing PDF…');const out=await PDFDocument.load(bytes),fc={},csC=new Map();let warn=false;out.registerFontkit(fontkit);
247
  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}))}
248
  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]))};
249
  for(const o of objs){
 
260
  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)});
261
  }
262
  const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([await out.save()],{type:'application/pdf'}));a.download='PDFix-edited.pdf';a.click();
263
+ status('Exported.');toast(warn?'Exported. Some characters are not supported by the font and became “?”.':'Edited PDF exported.');
264
  }catch(e){console.error(e);toast('Export failed.');status('Export failed.')}};
265
  </script>
266
  </body>