Talha912 commited on
Commit
0fa1ef7
·
verified ·
1 Parent(s): d49189f

Create edit.html

Browse files
Files changed (1) hide show
  1. edit.html +207 -0
edit.html ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PDFix 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>