Talha912 commited on
Commit
dcb201b
·
verified ·
1 Parent(s): d142e2e

Update edit.html

Browse files
Files changed (1) hide show
  1. edit.html +247 -172
edit.html CHANGED
@@ -5,203 +5,278 @@
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://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
 
9
  <style>
10
- :root{--bg:#f4f7fb;--panel:#fff;--ink:#172033;--muted:#667085;--line:#e4e9f1;--blue:#2864e8;--blue2:#1747b8;--soft:#edf3ff;--red:#c93434;--shadow:0 8px 26px rgba(22,34,61,.08)}
11
  *{box-sizing:border-box}body{margin:0;font-family:Inter,Segoe UI,Arial,sans-serif;color:var(--ink);background:var(--bg)}
12
- button,input,select{font:inherit}button{cursor:pointer;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:9px;padding:9px 12px;font-weight:600}button:hover{border-color:#9ab5f7;background:#f8faff}button.primary{background:var(--blue);color:white;border-color:var(--blue)}button.primary:hover{background:var(--blue2)}button.danger{color:var(--red)}button:disabled{opacity:.45;cursor:not-allowed}
13
- header{height:64px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 22px;gap:12px;position:sticky;top:0;z-index:5}
14
- .brand{display:flex;align-items:center;gap:10px;font-size:20px;font-weight:850;letter-spacing:-.5px}.logo{width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,#2864e8,#7c4dff);display:grid;place-items:center;color:#fff;font-weight:900}.brand small{display:block;font-size:10px;letter-spacing:1.8px;color:var(--muted);font-weight:700;margin-top:2px}
15
- .header-actions{display:flex;gap:8px;align-items:center}.header-actions input{display:none}
16
- .app{display:grid;grid-template-columns:235px minmax(300px,1fr) 270px;min-height:calc(100vh - 64px)}
17
- .sidebar,.properties{background:#fff;min-height:calc(100vh - 64px);padding:18px;border-right:1px solid var(--line)}.properties{border-right:0;border-left:1px solid var(--line)}
18
- .panel-title{font-size:11px;font-weight:800;letter-spacing:1.2px;color:var(--muted);text-transform:uppercase;margin:3px 0 14px}
19
- .tool-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.tool{padding:11px 7px;min-height:68px;text-align:center;font-size:12px;line-height:1.3}.tool .ico{display:block;font-size:20px;margin-bottom:5px}.tool.active{background:var(--soft);border-color:#aac1ff;color:var(--blue)}
20
- .sep{height:1px;background:var(--line);margin:18px 0}.field{margin:12px 0}.field label{display:block;font-size:12px;font-weight:700;color:#475467;margin-bottom:6px}.field input:not([type=color]):not([type=checkbox]),.field select{width:100%;border:1px solid var(--line);border-radius:8px;padding:9px;background:white;color:var(--ink)}.field input[type=color]{width:100%;height:38px;border:1px solid var(--line);border-radius:8px;background:white;padding:3px}.field input[type=range]{width:100%}.hint{font-size:11px;line-height:1.5;color:var(--muted)}
21
- .workspace{min-width:0;display:flex;flex-direction:column}.toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:7px;padding:12px 16px;background:#fff;border-bottom:1px solid var(--line)}.toolbar .spacer{flex:1}.toolbar button{padding:8px 10px;font-size:12px}.status{font-size:12px;color:var(--muted);padding:9px 16px;border-bottom:1px solid var(--line);background:#fbfcfe}
22
- .canvas-area{flex:1;overflow:auto;padding:30px 18px 70px;display:flex;align-items:flex-start;justify-content:center;position:relative}.welcome{max-width:540px;width:100%;margin:7vh auto;text-align:center;background:#fff;border:1px solid var(--line);border-radius:20px;padding:40px 30px;box-shadow:var(--shadow)}.welcome .big{font-size:52px}.welcome h1{font-size:26px;margin:14px 0 8px}.welcome p{color:var(--muted);line-height:1.6}.upload-label{display:inline-block;margin-top:12px}
23
- .page-wrap{position:relative;background:#fff;box-shadow:0 8px 30px rgba(22,34,61,.15);line-height:0;max-width:100%;user-select:none}.page-wrap canvas{display:block;max-width:100%;height:auto}.overlay{position:absolute;inset:0;overflow:hidden;touch-action:none}.item{position:absolute;cursor:move;line-height:normal;user-select:none;white-space:pre-wrap}.item.selected{outline:1px dashed var(--blue);outline-offset:2px}.item.text{min-width:25px;min-height:1em}.item.image img{display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}.item.shape{border:2px solid;pointer-events:auto}.item.highlight{opacity:.42}.item.signature{font-family:cursive}
24
- .thumb-list{display:flex;flex-direction:column;gap:10px;max-height:40vh;overflow:auto}.thumb{display:grid;grid-template-columns:54px 1fr auto;align-items:center;gap:9px;border:1px solid var(--line);padding:8px;border-radius:9px;cursor:pointer;font-size:12px}.thumb.active{border-color:var(--blue);background:var(--soft)}.thumb canvas{width:50px;height:auto;border:1px solid #e8ebf0}.thumb button{padding:3px 6px;font-size:11px}
25
- .page-actions{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:10px}.page-actions button{font-size:11px;padding:8px 4px}
26
- .empty-prop{padding:15px;border-radius:10px;background:#f7f9fc;color:var(--muted);font-size:12px;line-height:1.5}
27
- .footer-note{font-size:10px;color:var(--muted);line-height:1.5;margin-top:16px}
28
- #fileInput,#imageInput{display:none}.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#172033;color:white;padding:12px 16px;border-radius:10px;font-size:13px;box-shadow:var(--shadow);z-index:20;display:none;max-width:90vw}
29
- @media(max-width:1050px){.app{grid-template-columns:200px minmax(250px,1fr)}.properties{grid-column:1/-1;min-height:auto;border-left:0;border-top:1px solid var(--line)}.properties .prop-layout{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.properties .panel-title{grid-column:1/-1}}
30
- @media(max-width:700px){header{padding:0 12px}.brand{font-size:17px}.header-actions button{padding:8px;font-size:11px}.app{display:flex;flex-direction:column}.sidebar,.properties{min-height:auto;border-right:0;border-left:0;border-bottom:1px solid var(--line)}.tool-grid{grid-template-columns:repeat(4,1fr)}.tool{font-size:10px;padding:8px 3px}.tool .ico{font-size:17px}.thumb-list{flex-direction:row;max-height:none;overflow-x:auto}.thumb{min-width:145px}.properties .prop-layout{grid-template-columns:1fr 1fr}.canvas-area{padding:18px 8px 40px}.toolbar{padding:9px;gap:5px}.toolbar button{padding:7px;font-size:11px}}
 
 
 
 
 
31
  </style>
32
  </head>
33
  <body>
34
  <header>
35
- <div class="brand"><div class="logo">P</div><div>PDFix<small>ADVANCED PDF EDITOR</small></div></div>
36
- <div class="header-actions">
37
- <button id="openBtn">Open PDF</button>
38
- <button id="undoBtn" disabled>Undo</button>
39
- <button class="primary" id="saveBtn" disabled>Export PDF</button>
40
- </div>
41
  </header>
42
- <input id="fileInput" type="file" accept="application/pdf,.pdf">
43
- <input id="imageInput" type="file" accept="image/png,image/jpeg">
44
  <div class="app">
45
- <aside class="sidebar">
46
- <div class="panel-title">Editing tools</div>
47
- <div class="tool-grid">
48
- <button class="tool active" data-mode="select"><span class="ico">↖</span>Select / Move</button>
49
- <button class="tool" data-mode="text"><span class="ico">T</span>Add text</button>
50
- <button class="tool" data-mode="replace"><span class="ico">✎</span>Edit existing text</button>
51
- <button class="tool" data-mode="image"><span class="ico">▧</span>Add image</button>
52
- <button class="tool" data-mode="highlight"><span class="ico">🖍</span>Highlight</button>
53
- <button class="tool" data-mode="draw"><span class="ico">✎</span>Freehand</button>
54
- <button class="tool" data-mode="rect"><span class="ico">▱</span>Rectangle</button>
55
- <button class="tool" data-mode="whiteout"><span class="ico">□</span>Cover content</button>
56
- <button class="tool" data-mode="signature"><span class="ico">✍</span>Signature text</button>
57
  </div>
58
- <div class="sep"></div>
59
- <div class="panel-title">PDF pages</div>
60
- <div id="thumbList" class="thumb-list"><div class="hint">Open a PDF to see its pages.</div></div>
61
- <div class="page-actions">
62
- <button id="rotateBtn" disabled>↻ Rotate page</button>
63
- <button id="deletePageBtn" disabled class="danger">Delete page</button>
64
- <button id="duplicateBtn" disabled>Duplicate page</button>
65
- <button id="extractBtn" disabled>Extract page</button>
66
- </div>
67
- <div class="footer-note">Tip: Select an object to move, resize (using the properties panel), recolour or delete it. Use Export PDF to save a new copy.</div>
68
  </aside>
69
-
70
- <main class="workspace">
71
- <div class="toolbar">
72
- <button id="prevBtn" disabled>← Prev</button><button id="nextBtn" disabled>Next →</button>
73
- <span id="pageLabel" class="hint">No PDF open</span>
74
- <span class="spacer"></span>
75
- <button id="zoomOut" disabled>−</button><button id="zoomLabel" disabled>100%</button><button id="zoomIn" disabled>+</button>
76
- <button id="fitBtn" disabled>Fit page</button>
77
  </div>
78
- <div class="status" id="status">Open a PDF to begin. To replace existing text, choose “Edit existing text” and click a text line. Your file is processed in this browser.</div>
79
- <div class="canvas-area" id="canvasArea">
80
- <div class="welcome" id="welcome">
81
- <div class="big">📄</div><h1>Edit PDFs with PDFix</h1>
82
- <p>Add text, images, highlights, shapes and signatures; manage pages; then export your edited PDF.</p>
83
- <label class="upload-label"><button class="primary" id="welcomeOpen">Choose a PDF</button></label>
84
- <p class="hint">Supports ordinary PDF files. Internet is needed to load the PDF editing libraries.</p>
85
- </div>
86
  </div>
87
  </main>
88
-
89
- <aside class="properties">
90
- <div class="panel-title">Properties</div>
91
- <div id="noSelection" class="empty-prop">Select an added object to edit its properties. Choose a tool and click the PDF page to add content.</div>
92
- <div id="propPanel" hidden>
93
- <div class="prop-layout">
94
- <div class="field"><label>Text</label><input id="propText" type="text" placeholder="Text content"></div>
95
- <div class="field"><label>Font size</label><input id="propSize" type="number" min="6" max="144" value="18"></div>
96
- <div class="field"><label>Text / line colour</label><input id="propColor" type="color" value="#182033"></div>
97
- <div class="field"><label>Background / fill</label><input id="propFill" type="color" value="#fff176"></div>
98
- <div class="field"><label>Width (px)</label><input id="propWidth" type="number" min="10" max="2000" value="160"></div>
99
- <div class="field"><label>Height (px)</label><input id="propHeight" type="number" min="10" max="2000" value="44"></div>
100
- </div>
101
- <div class="field"><label>Transparency / opacity</label><input id="propOpacity" type="range" min="10" max="100" value="45"></div>
102
- <div class="field"><label>Text style</label><select id="propWeight"><option value="400">Regular</option><option value="700">Bold</option><option value="italic">Italic</option></select></div>
103
- <div class="field"><label>Position X (px)</label><input id="propX" type="number" min="0" value="30"></div>
104
- <div class="field"><label>Position Y (px)</label><input id="propY" type="number" min="0" value="30"></div>
105
- <button id="applyProps" class="primary" style="width:100%">Apply properties</button>
106
- <button id="deleteObject" class="danger" style="width:100%;margin-top:8px">Delete selected object</button>
107
  </div>
108
- <div class="sep"></div>
109
- <div class="panel-title">Quick actions</div>
110
- <div class="field"><label>Insert blank page after current</label><button id="insertBlank" style="width:100%" disabled>+ Insert blank page</button></div>
111
- <div class="field"><label>Page navigation</label><div style="display:flex;gap:8px"><input id="jumpPage" type="number" min="1" value="1" style="width:75px;padding:8px;border:1px solid var(--line);border-radius:8px"><button id="jumpBtn" disabled>Go</button></div></div>
112
- <div class="footer-note">Text replacement is visual: PDFix detects text lines and lets you click one to cover it and place replacement text. This does not rewrite the PDF's native text objects, may not perfectly match fonts/spacing, and is not secure redaction. Complex layouts may need manual adjustment.</div>
113
  </aside>
114
  </div>
115
  <div id="toast" class="toast"></div>
116
  <script>
117
  pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
118
- const { PDFDocument, rgb, StandardFonts, degrees } = PDFLib;
119
- const $=id=>document.getElementById(id);
120
- let originalBytes=null, pdfjsDoc=null, pdfDoc=null, currentPage=1, zoom=1, mode='select', selected=null, objects=[], undoStack=[], rotations={}, pageCount=0, renderTask=null;
121
- let pageWrap=null, overlay=null, pageCanvas=null, baseWidth=0, baseHeight=0, drawing=false, drawPoints=[];
122
- const colors={};
123
- function toast(msg){const t=$('toast');t.textContent=msg;t.style.display='block';clearTimeout(toast.timer);toast.timer=setTimeout(()=>t.style.display='none',3200)}
124
- function setStatus(s){$('status').textContent=s}
125
- function snapshot(){undoStack.push(JSON.stringify({objects,rotations,pageCount,currentPage}));if(undoStack.length>30)undoStack.shift();$('undoBtn').disabled=!undoStack.length}
126
- function restoreSnapshot(s){const d=JSON.parse(s);objects=d.objects||[];rotations=d.rotations||{};pageCount=d.pageCount||pageCount;currentPage=Math.min(d.currentPage||1,pageCount);selected=null;renderThumbs();renderPage();syncProps();updateControls()}
127
- function updateControls(){const loaded=!!pdfDoc;['prevBtn','nextBtn','zoomOut','zoomLabel','zoomIn','fitBtn','saveBtn','rotateBtn','deletePageBtn','duplicateBtn','extractBtn','insertBlank','jumpBtn'].forEach(id=>$(id).disabled=!loaded);$('pageLabel').textContent=loaded?`Page ${currentPage} of ${pageCount}`:'No PDF open';$('jumpPage').max=pageCount||1;$('jumpPage').value=currentPage;$('zoomLabel').textContent=Math.round(zoom*100)+'%';$('prevBtn').disabled=!loaded||currentPage<=1;$('nextBtn').disabled=!loaded||currentPage>=pageCount}
128
- function setMode(m){mode=m;document.querySelectorAll('.tool').forEach(b=>b.classList.toggle('active',b.dataset.mode===m));document.querySelectorAll('.existing-text-target').forEach(el=>el.style.pointerEvents=m==='replace'?'auto':'none');if(m==='image'){$('imageInput').click();return}setStatus(({select:'Select and move an added object.',text:'Click on the page to place text.',replace:'Click a detected text line to replace it. Replacement covers the original line visually.',highlight:'Drag on the page to add a highlight.',draw:'Drag on the page to draw.',rect:'Drag on the page to add a rectangle.',whiteout:'Drag over content to cover it with white.',signature:'Click on the page to place signature text.'})[m]||'Choose a tool.')}
129
- document.querySelectorAll('.tool').forEach(b=>b.addEventListener('click',()=>setMode(b.dataset.mode)));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  $('openBtn').onclick=$('welcomeOpen').onclick=()=>$('fileInput').click();
131
- $('fileInput').addEventListener('change',async e=>{const f=e.target.files[0];if(!f)return;try{setStatus('Opening PDF…');originalBytes=await f.arrayBuffer();pdfjsDoc=await pdfjsLib.getDocument({data:new Uint8Array(originalBytes.slice(0))}).promise;pdfDoc=await PDFDocument.load(originalBytes.slice(0));pageCount=pdfjsDoc.numPages;currentPage=1;zoom=1;objects=[];rotations={};undoStack=[];selected=null;$('welcome').style.display='none';renderThumbs();await renderPage();updateControls();$('undoBtn').disabled=true;setStatus(`${f.name} opened · ${pageCount} page(s) · Browser-based editing`);toast('PDF opened successfully.')}catch(err){console.error(err);toast('Could not open this PDF. It may be encrypted or unsupported.');setStatus('Could not open PDF. Try another file.')}e.target.value=''});
132
- async function renderPage(){if(!pdfjsDoc)return;if(renderTask){try{renderTask.cancel()}catch(e){}}const page=await pdfjsDoc.getPage(currentPage);const base=page.getViewport({scale:1,rotation:rotations[currentPage]||0});const vp=page.getViewport({scale:zoom,rotation:rotations[currentPage]||0});baseWidth=base.width;baseHeight=base.height;$('canvasArea').querySelectorAll('.page-wrap').forEach(n=>n.remove());pageWrap=document.createElement('div');pageWrap.className='page-wrap';pageWrap.style.width=vp.width+'px';pageWrap.style.maxWidth='100%';pageCanvas=document.createElement('canvas');pageCanvas.width=Math.ceil(vp.width);pageCanvas.height=Math.ceil(vp.height);pageCanvas.style.width=vp.width+'px';pageCanvas.style.maxWidth='100%';pageWrap.appendChild(pageCanvas);overlay=document.createElement('div');overlay.className='overlay';overlay.style.width=vp.width+'px';overlay.style.height=vp.height+'px';pageWrap.appendChild(overlay);$('canvasArea').appendChild(pageWrap);renderTask=page.render({canvasContext:pageCanvas.getContext('2d'),viewport:vp});try{await renderTask.promise}catch(e){};renderOverlay();await renderExistingTextTargets(page,vp);updateControls()}
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- async function renderExistingTextTargets(page, viewport){
135
- if(!overlay || !pdfjsDoc) return;
136
- // Transparent hit areas let the user click an existing PDF text line.
137
- // Export uses a white cover plus replacement text; it does not rewrite the PDF's native text objects.
138
- try {
139
- const content = await page.getTextContent();
140
- const frag = document.createDocumentFragment();
141
- for (const item of content.items) {
142
- if (!item.str || !item.str.trim() || !item.transform) continue;
143
- const tx = PDFJSUtilTransform(viewport, item.transform);
144
- const height = Math.max(8, Math.hypot(item.transform[2], item.transform[3]) * zoom);
145
- const width = Math.max(10, (item.width || item.str.length * 5) * zoom);
146
- const left = tx.x / zoom;
147
- const top = (tx.y - height) / zoom;
148
- const target = document.createElement('div');
149
- target.className = 'existing-text-target';
150
- target.title = 'Click to replace: ' + item.str;
151
- target.style.cssText = `position:absolute;left:${left}px;top:${top}px;width:${width/zoom}px;height:${height/zoom}px;z-index:2;cursor:text;background:transparent;border:1px solid transparent;line-height:1;overflow:hidden;pointer-events:${mode==='replace'?'auto':'none'};`;
152
- target.addEventListener('mouseenter',()=>{if(mode==='replace')target.style.background='rgba(40,100,232,.12)';});
153
- target.addEventListener('mouseleave',()=>{target.style.background='transparent';});
154
- target.addEventListener('pointerdown',e=>{
155
- if(mode!=='replace') return;
156
- e.preventDefault();e.stopPropagation();
157
- const replacement=prompt('Replace this text line:',item.str);
158
- if(replacement===null || replacement===item.str) return;
159
- if(!replacement.trim()){toast('Blank replacement cancelled. Use a white cover if you want to hide text.');return;}
160
- const x=Math.max(0,left), y=Math.max(0,top), w=Math.max(20,width/zoom+4), h=Math.max(12,height/zoom+4);
161
- snapshot();
162
- objects.push({id:(crypto.randomUUID?crypto.randomUUID():String(Date.now()+Math.random())),page:currentPage,type:'whiteout',x,y,w,h,fill:'#ffffff',color:'#ffffff',opacity:1});
163
- objects.push({id:(crypto.randomUUID?crypto.randomUUID():String(Date.now()+Math.random())),page:currentPage,type:'text',x:x+1,y:y+1,w:Math.max(w,replacement.length*7),h,text:replacement,size:Math.max(8,height/zoom*.78),color:'#182033',fill:'transparent',opacity:1,weight:'400'});
164
- selected=objects[objects.length-1].id;
165
- renderOverlay();syncProps();
166
- toast('Text replacement added. Export PDF to save the visible replacement.');
167
- });
168
- frag.appendChild(target);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
170
- overlay.appendChild(frag);
171
- } catch(err) { console.warn('Could not detect text on page',err); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
- function PDFJSUtilTransform(viewport, transform){
174
- const point = viewport.convertToViewportPoint(transform[4], transform[5]);
175
- return {x:point[0], y:point[1]};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- function pageObjects(){return objects.filter(o=>o.page===currentPage)}
179
- function renderOverlay(){if(!overlay)return;overlay.innerHTML='';for(const o of pageObjects()){const el=document.createElement('div');el.className='item '+o.type+(selected===o.id?' selected':'');el.dataset.id=o.id;el.style.left=o.x*zoom+'px';el.style.top=o.y*zoom+'px';el.style.width=o.w*zoom+'px';el.style.height=o.h*zoom+'px';el.style.opacity=o.opacity??1;if(o.type==='text'||o.type==='signature'){el.textContent=o.text||'Text';el.style.fontSize=(o.size||18)*zoom+'px';el.style.color=o.color||'#182033';el.style.fontWeight=o.weight==='italic'?'400':(o.weight||'400');el.style.fontStyle=o.weight==='italic'?'italic':'normal';el.style.fontFamily=o.type==='signature'?'cursive':'Arial,sans-serif';el.style.background=o.fill&&o.fill!=='transparent'?o.fill:'transparent';el.style.padding='1px 2px';}else if(o.type==='image'){const img=document.createElement('img');img.src=o.src;el.appendChild(img)}else if(o.type==='highlight'){el.style.background=o.fill||'#fff176';el.style.opacity=o.opacity??.42}else if(o.type==='whiteout'){el.style.background='#fff';el.style.border='1px solid #ddd'}else if(o.type==='rect'){el.style.border=`${Math.max(1,(o.size||2)*zoom)}px solid ${o.color||'#2864e8'}`;el.style.background=o.fill&&o.fill!=='transparent'?o.fill:'transparent'}else if(o.type==='draw'){el.style.pointerEvents='none';const c=document.createElement('canvas');c.width=Math.max(1,o.w*zoom);c.height=Math.max(1,o.h*zoom);c.style.width='100%';c.style.height='100%';el.appendChild(c);const ctx=c.getContext('2d');ctx.strokeStyle=o.color||'#2864e8';ctx.lineWidth=(o.size||3)*zoom;ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();(o.points||[]).forEach((p,i)=>{if(!i)ctx.moveTo((p.x-o.x)*zoom,(p.y-o.y)*zoom);else ctx.lineTo((p.x-o.x)*zoom,(p.y-o.y)*zoom)});ctx.stroke();}
180
- el.addEventListener('pointerdown',e=>{if(mode!=='select')return;e.stopPropagation();selected=o.id;syncProps();renderOverlay();const startX=e.clientX,startY=e.clientY,ox=o.x,oy=o.y;el.setPointerCapture?.(e.pointerId);const move=ev=>{o.x=Math.max(0,ox+(ev.clientX-startX)/zoom);o.y=Math.max(0,oy+(ev.clientY-startY)/zoom);el.style.left=o.x*zoom+'px';el.style.top=o.y*zoom+'px';syncPositionFields()};const up=()=>{window.removeEventListener('pointermove',move);window.removeEventListener('pointerup',up)};window.addEventListener('pointermove',move);window.addEventListener('pointerup',up);});overlay.appendChild(el)}}
181
- function syncProps(){const o=objects.find(x=>x.id===selected);$('noSelection').hidden=!!o;$('propPanel').hidden=!o;if(!o)return;$('propText').value=o.text||'';$('propSize').value=o.size||18;$('propColor').value=o.color||'#182033';$('propFill').value=o.fill&&o.fill!=='transparent'?o.fill:'#fff176';$('propWidth').value=Math.round(o.w);$('propHeight').value=Math.round(o.h);$('propOpacity').value=Math.round((o.opacity??(o.type==='highlight'?.42:1))*100);$('propWeight').value=o.weight||'400';$('propX').value=Math.round(o.x);$('propY').value=Math.round(o.y)}
182
- function syncPositionFields(){if(!selected)return;$('propX').value=Math.round(objects.find(o=>o.id===selected)?.x||0);$('propY').value=Math.round(objects.find(o=>o.id===selected)?.y||0)}
183
- function addObject(o){snapshot();o.id=crypto.randomUUID?crypto.randomUUID():String(Date.now()+Math.random());o.page=currentPage;objects.push(o);selected=o.id;renderOverlay();syncProps();$('saveBtn').disabled=false}
184
- function xy(e){const r=overlay.getBoundingClientRect();return{x:(e.clientX-r.left)/zoom,y:(e.clientY-r.top)/zoom}}
185
- overlayEventSetup();
186
- function overlayEventSetup(){document.addEventListener('pointerdown',e=>{if(!overlay||!overlay.contains(e.target)||e.target.closest('.item'))return;const p=xy(e);if(mode==='select'){selected=null;syncProps();renderOverlay();return}if(mode==='text'||mode==='signature'){const text=mode==='signature'?'Signature':prompt('Enter text to add to the PDF:','Your text');if(text===null||text==='')return;addObject({type:mode,x:p.x,y:p.y,w:Math.max(120,text.length*10),h:34,text,size:mode==='signature'?26:18,color:'#182033',fill:'transparent',opacity:1,weight:mode==='signature'?'italic':'400'});return}if(mode==='image'){toast('Choose an image file to insert.');$('imageInput').click();return}if(['highlight','draw','rect','whiteout'].includes(mode)){drawing=true;drawPoints=[p];const ghost=document.createElement('div');ghost.className='item';ghost.id='drawingGhost';ghost.style.left=p.x*zoom+'px';ghost.style.top=p.y*zoom+'px';ghost.style.width='1px';ghost.style.height='1px';ghost.style.pointerEvents='none';ghost.style.border=mode==='whiteout'?'1px solid #bbb':'1px dashed #2864e8';ghost.style.background=mode==='highlight'?'#fff17666':mode==='whiteout'?'#fff':'transparent';overlay.appendChild(ghost);const move=ev=>{if(!drawing)return;const q=xy(ev);drawPoints.push(q);const x=Math.min(p.x,q.x),y=Math.min(p.y,q.y),w=Math.max(2,Math.abs(q.x-p.x)),h=Math.max(2,Math.abs(q.y-p.y));ghost.style.left=x*zoom+'px';ghost.style.top=y*zoom+'px';ghost.style.width=w*zoom+'px';ghost.style.height=h*zoom+'px';if(mode==='draw'){ghost.style.border='0';ghost.style.background='transparent'}};const up=ev=>{if(!drawing)return;drawing=false;window.removeEventListener('pointermove',move);window.removeEventListener('pointerup',up);ghost.remove();const q=xy(ev),x=Math.min(p.x,q.x),y=Math.min(p.y,q.y),w=Math.max(2,Math.abs(q.x-p.x)),h=Math.max(2,Math.abs(q.y-p.y));if(mode==='draw'){const pts=drawPoints.length>1?drawPoints:[p,q];const minX=Math.min(...pts.map(a=>a.x)),minY=Math.min(...pts.map(a=>a.y));addObject({type:'draw',x:minX,y:minY,w:Math.max(2,Math.max(...pts.map(a=>a.x))-minX),h:Math.max(2,Math.max(...pts.map(a=>a.y))-minY),points:pts,color:'#2864e8',size:2,opacity:1})}else addObject({type:mode,x,y,w,h,color:'#2864e8',fill:mode==='highlight'?'#fff176':mode==='whiteout'?'#ffffff':'transparent',size:2,opacity:mode==='highlight'?.42:1});};window.addEventListener('pointermove',move);window.addEventListener('pointerup',up,{once:true})}})}
187
- $('imageInput').addEventListener('change',async e=>{const f=e.target.files[0];if(!f)return;const src=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(r.result);r.onerror=rej;r.readAsDataURL(f)});const img=new Image();img.onload=()=>{addObject({type:'image',x:35,y:35,w:Math.min(260,img.width),h:Math.min(180,img.height),src,opacity:1});setMode('select');toast('Image added. Move it and adjust its size in Properties.')};img.src=src;e.target.value=''});
188
- $('applyProps').onclick=()=>{const o=objects.find(x=>x.id===selected);if(!o)return;snapshot();o.text=$('propText').value;o.size=+ $('propSize').value||18;o.color=$('propColor').value;o.fill=(o.type==='text'||o.type==='signature')?'transparent':$('propFill').value;o.w=Math.max(10,+$('propWidth').value||100);o.h=Math.max(10,+$('propHeight').value||30);o.opacity=(+$('propOpacity').value||100)/100;o.weight=$('propWeight').value;o.x=Math.max(0,+$('propX').value||0);o.y=Math.max(0,+$('propY').value||0);renderOverlay();toast('Properties applied.')};
189
- $('deleteObject').onclick=()=>{if(!selected)return;snapshot();objects=objects.filter(o=>o.id!==selected);selected=null;renderOverlay();syncProps();toast('Object deleted.')};
190
- $('undoBtn').onclick=()=>{const s=undoStack.pop();if(s)restoreSnapshot(s);$('undoBtn').disabled=!undoStack.length};
191
- $('prevBtn').onclick=()=>{if(currentPage>1){currentPage--;selected=null;renderPage();syncProps();renderThumbs()}};
192
- $('nextBtn').onclick=()=>{if(currentPage<pageCount){currentPage++;selected=null;renderPage();syncProps();renderThumbs()}};
193
- $('zoomIn').onclick=()=>{zoom=Math.min(2.5,zoom+.1);renderPage()};$('zoomOut').onclick=()=>{zoom=Math.max(.35,zoom-.1);renderPage()};
194
- $('fitBtn').onclick=()=>{if(!pdfjsDoc)return;const p=pdfjsDoc.getPage(currentPage);p.then(pg=>{const v=pg.getViewport({scale:1,rotation:rotations[currentPage]||0});zoom=Math.min(1.5,($('canvasArea').clientWidth-50)/v.width);renderPage()})};
195
- $('jumpBtn').onclick=()=>{const n=+$('jumpPage').value;if(n>=1&&n<=pageCount){currentPage=n;selected=null;renderPage();renderThumbs();syncProps()}};
196
- async function renderThumbs(){const list=$('thumbList');list.innerHTML='';if(!pdfjsDoc)return;for(let i=1;i<=pageCount;i++){const row=document.createElement('div');row.className='thumb'+(i===currentPage?' active':'');const c=document.createElement('canvas');const label=document.createElement('span');label.textContent=`Page ${i}`;const pick=document.createElement('button');pick.textContent='Go';pick.onclick=e=>{e.stopPropagation();currentPage=i;selected=null;renderPage();renderThumbs();syncProps()};row.onclick=()=>{currentPage=i;selected=null;renderPage();renderThumbs();syncProps()};row.append(c,label,pick);list.appendChild(row);try{const pg=await pdfjsDoc.getPage(i),vp=pg.getViewport({scale:.14,rotation:rotations[i]||0});c.width=vp.width;c.height=vp.height;await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise}catch(e){}}}
197
- async function rebuildPdf(){const out=await PDFDocument.create();const source=await PDFDocument.load(originalBytes.slice(0));for(let i=0;i<pageCount;i++){const [p]=await out.copyPages(source,[i]);out.addPage(p)}for(let i=0;i<pageCount;i++){const p=out.getPage(i);const rot=rotations[i+1]||0;if(rot)p.setRotation(degrees(rot));}const font=await out.embedFont(StandardFonts.Helvetica),bold=await out.embedFont(StandardFonts.HelveticaBold),italic=await out.embedFont(StandardFonts.HelveticaOblique);for(const o of objects){if(o.page<1||o.page>pageCount)continue;const p=out.getPage(o.page-1),sz=p.getSize(),scaleX=sz.width/baseWidth,scaleY=sz.height/baseHeight;const x=o.x*scaleX,y=sz.height-(o.y+o.h)*scaleY,w=o.w*scaleX,h=o.h*scaleY;const col=hexRgb(o.color||'#182033'),fill=hexRgb(o.fill||'#ffffff'),opacity=o.opacity??1;try{if(o.type==='text'||o.type==='signature'){const f=o.weight==='italic'?italic:o.weight==='700'?bold:font;const lines=String(o.text||'').split('\\n');lines.forEach((line,idx)=>p.drawText(line,{x:x+2,y:sz.height-o.y*scaleY-(o.size||18)*scaleY*(idx+1),size:(o.size||18)*scaleY,font:f,color:rgb(...col),opacity}))}else if(o.type==='image'){let img;const data=o.src.split(',')[1];if(o.src.startsWith('data:image/png'))img=await out.embedPng(Uint8Array.from(atob(data),c=>c.charCodeAt(0)));else img=await out.embedJpg(Uint8Array.from(atob(data),c=>c.charCodeAt(0)));p.drawImage(img,{x,y,width:w,height:h,opacity})}else if(o.type==='whiteout'){p.drawRectangle({x,y,width:w,height:h,color:rgb(1,1,1),borderColor:rgb(.85,.85,.85),borderWidth:.4})}else if(o.type==='highlight'){p.drawRectangle({x,y,width:w,height:h,color:rgb(...fill),opacity:Math.min(.55,opacity),borderWidth:0})}else if(o.type==='rect'){p.drawRectangle({x,y,width:w,height:h,borderColor:rgb(...col),borderWidth:(o.size||2),color:o.fill&&o.fill!=='transparent'?rgb(...fill):undefined,opacity})}else if(o.type==='draw'){const pts=o.points||[];for(let j=1;j<pts.length;j++){p.drawLine({start:{x:pts[j-1].x*scaleX,y:sz.height-pts[j-1].y*scaleY},end:{x:pts[j].x*scaleX,y:sz.height-pts[j].y*scaleY},thickness:o.size||2,color:rgb(...col),opacity})}}}catch(err){console.warn('Could not export object',o,err)}}return out}
198
- function hexRgb(hex){let h=String(hex||'#000000').replace('#','');if(h.length===3)h=h.split('').map(c=>c+c).join('');const n=parseInt(h.slice(0,6),16)||0;return[((n>>16)&255)/255,((n>>8)&255)/255,(n&255)/255]}
199
- $('saveBtn').onclick=async()=>{try{setStatus('Preparing edited PDF…');const out=await rebuildPdf();const bytes=await out.save();const blob=new Blob([bytes],{type:'application/pdf'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download='PDFix-edited.pdf';a.click();URL.revokeObjectURL(url);setStatus('Export complete. Your edited PDF has been downloaded.');toast('Edited PDF exported.')}catch(e){console.error(e);toast('Export failed. Try a simpler PDF or remove unsupported objects.');setStatus('Export failed.')}};
200
- $('rotateBtn').onclick=()=>{snapshot();rotations[currentPage]=((rotations[currentPage]||0)+90)%360;renderPage();renderThumbs()};
201
- $('deletePageBtn').onclick=async()=>{if(pageCount<=1){toast('A PDF must contain at least one page.');return}if(!confirm(`Delete page ${currentPage}?`))return;snapshot();const src=await PDFDocument.load(originalBytes.slice(0));src.removePage(currentPage-1);originalBytes=await src.save();pdfDoc=await PDFDocument.load(originalBytes);pdfjsDoc=await pdfjsLib.getDocument({data:new Uint8Array(originalBytes.slice(0))}).promise;pageCount=pdfjsDoc.numPages;objects=objects.filter(o=>o.page!==currentPage);objects.forEach(o=>{if(o.page>currentPage)o.page--});currentPage=Math.min(currentPage,pageCount);rotations={};renderThumbs();renderPage();toast('Page deleted. Note: existing overlay positions are retained on remaining pages.')};
202
- $('duplicateBtn').onclick=async()=>{try{snapshot();const src=await PDFDocument.load(originalBytes.slice(0));const [cp]=await src.copyPages(src,[currentPage-1]);src.insertPage(currentPage,cp);originalBytes=await src.save();pdfDoc=await PDFDocument.load(originalBytes);pdfjsDoc=await pdfjsLib.getDocument({data:new Uint8Array(originalBytes.slice(0))}).promise;pageCount=pdfjsDoc.numPages;objects.forEach(o=>{if(o.page>currentPage)o.page++});currentPage++;renderThumbs();renderPage();toast('Page duplicated.')}catch(e){toast('Could not duplicate this page.')}};
203
- $('extractBtn').onclick=async()=>{try{const src=await PDFDocument.load(originalBytes.slice(0));const out=await PDFDocument.create();const [p]=await out.copyPages(src,[currentPage-1]);out.addPage(p);const bytes=await out.save();const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([bytes],{type:'application/pdf'}));a.download=`PDFix-page-${currentPage}.pdf`;a.click();toast('Current page extracted.')}catch(e){toast('Page extraction failed.')}};
204
- $('insertBlank').onclick=async()=>{try{snapshot();const src=await PDFDocument.load(originalBytes.slice(0));src.insertPage(currentPage,[612,792]);originalBytes=await src.save();pdfDoc=await PDFDocument.load(originalBytes);pdfjsDoc=await pdfjsLib.getDocument({data:new Uint8Array(originalBytes.slice(0))}).promise;pageCount=pdfjsDoc.numPages;objects.forEach(o=>{if(o.page>currentPage)o.page++});currentPage++;renderThumbs();renderPage();toast('Blank page inserted.')}catch(e){toast('Could not insert a page.')}};
205
  </script>
206
  </body>
207
  </html>
 
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>
92
  pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
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+=' ';
149
+ last.text+=it.str;last.w=x+it.width-last.x;continue}
150
+ let name='';try{name=pg.commonObjs.get(it.fontName).name||''}catch(e){}
151
+ last={x,y,w:it.width,size,font:it.fontName,text:it.str,name,ff:(tc.styles[it.fontName]||{}).fontFamily||''};out.push(last);
152
+ }
153
+ return out.filter(o=>o.text.trim());
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;
161
+ 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}}
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){
265
+ if(o.page<1||o.page>out.getPageCount())continue;
266
+ const p=out.getPage(o.page-1),{width:pw,height:ph}=p.getSize(),sx=pw/W,sy=ph/H;
267
+ if(o.type==='text'){const f=await getF(o);let cs=csC.get(f);if(!cs){cs=new Set(f.getCharacterSet());csC.set(f,cs)}
268
+ const safe=s=>[...s].map(c=>cs.has(c.codePointAt(0))?c:(warn=true,'?')).join('');
269
+ String(o.text).split('\n').forEach((ln,i)=>p.drawText(safe(ln),{x:o.x*sx,y:ph-(o.y+BL(o.family)*o.size+i*o.size*1.2)*sy,size:o.size*sy,font:f,color:hexC(o.color)}));continue}
270
+ const x=o.x*sx,y=ph-(o.y+o.h)*sy,w=o.w*sx,h=o.h*sy;
271
+ if(o.type==='cover')p.drawRectangle({x,y,width:w,height:h,color:hexC(o.color),borderWidth:0});
272
+ else if(o.type==='highlight')p.drawRectangle({x,y,width:w,height:h,color:hexC(o.color),opacity:.45,borderWidth:0});
273
+ else if(o.type==='rect')p.drawRectangle({x,y,width:w,height:h,borderColor:hexC(o.color),borderWidth:2});
274
+ 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})}
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>
282
  </html>