Update edit.html
Browse files
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://
|
|
|
|
| 9 |
<style>
|
| 10 |
-
:root{--bg:#f4f7fb;--
|
| 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:
|
| 13 |
-
header{height:
|
| 14 |
-
.brand{display:flex;align-items:center;gap:10px;font-size:20px;font-weight:
|
| 15 |
-
.
|
| 16 |
-
.app{display:grid;grid-template-columns:
|
| 17 |
-
.
|
| 18 |
-
.
|
| 19 |
-
.
|
| 20 |
-
.
|
| 21 |
-
.
|
| 22 |
-
.
|
| 23 |
-
.
|
| 24 |
-
.
|
| 25 |
-
.
|
| 26 |
-
.
|
| 27 |
-
.
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</style>
|
| 32 |
</head>
|
| 33 |
<body>
|
| 34 |
<header>
|
| 35 |
-
<div class="brand"><div class="logo">P</div>
|
| 36 |
-
<div class="
|
| 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="
|
| 46 |
-
<div class="
|
| 47 |
-
<div class="
|
| 48 |
-
<button class="tool active" data-mode="select"><
|
| 49 |
-
<button class="tool" data-mode="
|
| 50 |
-
<button class="tool" data-mode="
|
| 51 |
-
<button class="tool" data-mode="image"><
|
| 52 |
-
<button class="tool" data-mode="highlight"><
|
| 53 |
-
<button class="tool" data-mode="draw"><
|
| 54 |
-
<button class="tool" data-mode="rect"><
|
| 55 |
-
<button class="tool" data-mode="
|
| 56 |
-
<button class="tool" data-mode="signature"><span class="ico">✍</span>Signature text</button>
|
| 57 |
</div>
|
| 58 |
-
<div class="
|
| 59 |
-
<
|
| 60 |
-
<
|
| 61 |
-
<
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 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 |
-
|
| 71 |
-
|
| 72 |
-
<
|
| 73 |
-
<span id="
|
| 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.
|
| 79 |
-
<div class="
|
| 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 |
-
|
| 90 |
-
<div class="
|
| 91 |
-
<div id="
|
| 92 |
-
|
| 93 |
-
<div class="
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 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 |
-
<
|
| 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 {
|
| 119 |
-
const
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
const
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
$('openBtn').onclick=$('welcomeOpen').onclick=()=>$('fileInput').click();
|
| 131 |
-
$('fileInput').
|
| 132 |
-
async function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
const
|
| 140 |
-
const
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
}
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
}
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 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>
|