Skip to content

Commit fe2a6a1

Browse files
authored
Add files via upload
0 parents  commit fe2a6a1

1 file changed

Lines changed: 392 additions & 0 deletions

File tree

index.html

Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
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>Print Without Black Ink | Free PDF Color Converter</title>
7+
<meta name="description" content="Convert your PDFs to use color ink instead of black. 100% free, runs locally in your browser for total privacy.">
8+
9+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
10+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
11+
12+
<style>
13+
:root {
14+
--primary: #004aad;
15+
--primary-hover: #003380;
16+
--bg: #f8f9fc;
17+
--surface: #ffffff;
18+
--text: #333;
19+
--border: #e2e8f0;
20+
}
21+
body {
22+
font-family: system-ui, -apple-system, sans-serif;
23+
background-color: var(--bg);
24+
color: var(--text);
25+
margin: 0;
26+
padding: 2rem 1rem;
27+
display: flex;
28+
justify-content: center;
29+
}
30+
.container {
31+
background: var(--surface);
32+
padding: 2rem;
33+
border-radius: 12px;
34+
box-shadow: 0 10px 25px rgba(0,0,0,0.05);
35+
max-width: 900px;
36+
width: 100%;
37+
}
38+
h1 { color: var(--primary); margin-top: 0; text-align: center; }
39+
.hero-text { text-align: center; color: #555; margin-bottom: 2rem; }
40+
.badge { background: #e0f2fe; color: #0284c7; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold; }
41+
42+
.controls-grid {
43+
display: grid;
44+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
45+
gap: 1.5rem;
46+
margin-bottom: 1.5rem;
47+
background: var(--bg);
48+
padding: 1.5rem;
49+
border-radius: 8px;
50+
border: 1px solid var(--border);
51+
}
52+
label { font-weight: 600; display: block; margin-bottom: 0.5rem; font-size: 0.9rem; }
53+
select, input[type="file"] {
54+
width: 100%;
55+
padding: 10px;
56+
border: 1px solid var(--border);
57+
border-radius: 6px;
58+
box-sizing: border-box;
59+
background: white;
60+
}
61+
.checkbox-group { display: flex; align-items: center; gap: 8px; margin-top: 1rem; }
62+
.checkbox-group input { width: auto; margin: 0; }
63+
64+
details { background: white; border: 1px solid var(--border); border-radius: 6px; padding: 10px; margin-top: 1rem; }
65+
summary { font-weight: 600; cursor: pointer; color: #555; }
66+
.advanced-settings { margin-top: 1rem; display: flex; flex-direction: column; gap: 1rem; font-size: 0.9rem; }
67+
.warning-text { color: #b91c1c; font-size: 0.85rem; margin-top: 4px; }
68+
.help-text { color: #666; font-size: 0.85rem; margin-top: 4px; }
69+
70+
.btn-group { display: flex; gap: 10px; margin-top: 1.5rem; }
71+
button {
72+
flex: 1;
73+
background-color: var(--primary);
74+
color: white;
75+
border: none;
76+
padding: 14px 24px;
77+
border-radius: 8px;
78+
font-size: 1rem;
79+
font-weight: 600;
80+
cursor: pointer;
81+
transition: background 0.2s, opacity 0.2s;
82+
}
83+
button:hover { background-color: var(--primary-hover); }
84+
button:disabled { background-color: #cbd5e1; cursor: not-allowed; color: #64748b; }
85+
#downloadBtn { background-color: #059669; }
86+
#downloadBtn:hover { background-color: #047857; }
87+
88+
#status { text-align: center; margin-top: 1rem; font-weight: 600; color: #0284c7; }
89+
90+
/* Preview Section */
91+
.workspace { display: none; margin-top: 2rem; border-top: 2px solid var(--border); padding-top: 2rem; }
92+
.pagination { display: flex; justify-content: center; align-items: center; gap: 1rem; margin-bottom: 1rem; }
93+
.pagination button { flex: none; padding: 8px 16px; font-size: 0.9rem; width: auto; }
94+
95+
.preview-panels { display: flex; gap: 1rem; flex-wrap: wrap; }
96+
.panel { flex: 1; min-width: 300px; display: flex; flex-direction: column; align-items: center; }
97+
.panel h3 { font-size: 1rem; color: #555; margin-bottom: 0.5rem; }
98+
canvas {
99+
max-width: 100%;
100+
height: auto;
101+
border: 1px solid #ccc;
102+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
103+
background: white;
104+
}
105+
</style>
106+
</head>
107+
<body>
108+
109+
<div class="container">
110+
<h1>🖨️ Print Without Black</h1>
111+
<p class="hero-text">
112+
Printer refusing to print because the black ink is empty? <br>
113+
Convert your PDF to a deep blue to force the printer to use your color cartridges.
114+
<span class="badge">100% Free</span> <span class="badge">Private (Local Browser)</span>
115+
</p>
116+
117+
<div class="controls-grid">
118+
<div>
119+
<label for="pdfInput">1. Select your PDF Document</label>
120+
<input type="file" id="pdfInput" accept="application/pdf">
121+
122+
<div class="checkbox-group">
123+
<input type="checkbox" id="forceGrayscale" checked>
124+
<label for="forceGrayscale" style="margin:0; font-weight: normal;">
125+
Force Monochrome (Convert entire document to target color)
126+
</label>
127+
</div>
128+
<p class="help-text" style="margin-top:4px;">Uncheck this if your PDF has colors you want to keep, and you *only* want to change the black text.</p>
129+
</div>
130+
131+
<div>
132+
<label for="colorSelect">2. Choose Replacement Color</label>
133+
<select id="colorSelect">
134+
<option value="10,10,100">Deep Indigo (Best overall, uses Cyan + Magenta)</option>
135+
<option value="0,100,0">Forest Green (Use if Magenta is empty)</option>
136+
<option value="150,0,0">Maroon (Use if Cyan is empty)</option>
137+
<option value="100,0,150">Deep Violet (Use if Yellow is empty)</option>
138+
</select>
139+
140+
<details>
141+
<summary>⚙️ Advanced Quality Settings</summary>
142+
<div class="advanced-settings">
143+
<div>
144+
<label for="pdfScale">Render Scale: <span id="scaleVal">2.0</span>x</label>
145+
<input type="range" id="pdfScale" value="2.0" min="1.0" max="4.0" step="0.5" style="width:100%;">
146+
<p class="help-text">Higher scale = sharper text but much larger file sizes.</p>
147+
<p class="warning-text">Warning: Setting this above 3.0 may cause the browser to freeze on long documents.</p>
148+
</div>
149+
<div>
150+
<label for="pdfQuality">JPEG Compression: <span id="qualityVal">90</span>%</label>
151+
<input type="range" id="pdfQuality" value="0.90" min="0.5" max="1.0" step="0.05" style="width:100%;">
152+
<p class="help-text">Lower quality reduces file size but may introduce visual artifacts around text.</p>
153+
</div>
154+
</div>
155+
</details>
156+
</div>
157+
</div>
158+
159+
<div class="btn-group">
160+
<button id="previewBtn">Load & Preview</button>
161+
<button id="downloadBtn" disabled>Download Ready PDF</button>
162+
</div>
163+
164+
<div id="status"></div>
165+
166+
<div class="workspace" id="workspace">
167+
<div class="pagination">
168+
<button id="prevPageBtn" disabled>◀ Previous</button>
169+
<span id="pageIndicator" style="font-weight: bold;">Page 1 of ?</span>
170+
<button id="nextPageBtn">Next ▶</button>
171+
</div>
172+
173+
<div class="preview-panels">
174+
<div class="panel">
175+
<h3>Original</h3>
176+
<canvas id="originalCanvas"></canvas>
177+
</div>
178+
<div class="panel">
179+
<h3>Converted</h3>
180+
<canvas id="previewCanvas"></canvas>
181+
</div>
182+
</div>
183+
</div>
184+
</div>
185+
186+
<script>
187+
// Initialize PDF.js
188+
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
189+
190+
let loadedPdf = null;
191+
let originalFileName = "document";
192+
let currentPage = 1;
193+
194+
// UI Listeners for Advanced Settings
195+
document.getElementById('pdfScale').addEventListener('input', (e) => {
196+
document.getElementById('scaleVal').innerText = parseFloat(e.target.value).toFixed(1);
197+
});
198+
document.getElementById('pdfQuality').addEventListener('input', (e) => {
199+
document.getElementById('qualityVal').innerText = Math.round(parseFloat(e.target.value) * 100);
200+
});
201+
202+
// Color Tinting Logic
203+
function applyColorTint(ctx, width, height, targetRGB, forceMonochrome) {
204+
const imgData = ctx.getImageData(0, 0, width, height);
205+
const data = imgData.data;
206+
const [targetR, targetG, targetB] = targetRGB.split(',').map(Number);
207+
208+
for (let i = 0; i < data.length; i += 4) {
209+
let r = data[i], g = data[i+1], b = data[i+2];
210+
let gray = (r * 0.299 + g * 0.587 + b * 0.114);
211+
212+
if (forceMonochrome) {
213+
// Map all grays to the target color gradient
214+
data[i] = targetR + (255 - targetR) * (gray / 255);
215+
data[i+1] = targetG + (255 - targetG) * (gray / 255);
216+
data[i+2] = targetB + (255 - targetB) * (gray / 255);
217+
} else {
218+
// Smart mode: Only change dark/grayscale pixels. Preserve vibrant colors.
219+
// Calculate saturation (how different the max and min RGB values are)
220+
let max = Math.max(r, g, b);
221+
let min = Math.min(r, g, b);
222+
let saturation = max === 0 ? 0 : (max - min) / max;
223+
224+
// If it's desaturated (grayish) AND darkish, tint it
225+
if (saturation < 0.25 && gray < 200) {
226+
data[i] = targetR + (255 - targetR) * (gray / 255);
227+
data[i+1] = targetG + (255 - targetG) * (gray / 255);
228+
data[i+2] = targetB + (255 - targetB) * (gray / 255);
229+
}
230+
}
231+
}
232+
ctx.putImageData(imgData, 0, 0);
233+
}
234+
235+
// Render a specific page to the UI
236+
async function renderPageToUI(pageNum) {
237+
if (!loadedPdf) return;
238+
239+
const statusDiv = document.getElementById('status');
240+
const targetColor = document.getElementById('colorSelect').value;
241+
const forceMonochrome = document.getElementById('forceGrayscale').checked;
242+
243+
try {
244+
const page = await loadedPdf.getPage(pageNum);
245+
const viewport = page.getViewport({ scale: 1.5 }); // Fixed scale for screen preview
246+
247+
// Setup Original Canvas
248+
const origCanvas = document.getElementById('originalCanvas');
249+
const origCtx = origCanvas.getContext('2d');
250+
origCanvas.width = viewport.width;
251+
origCanvas.height = viewport.height;
252+
await page.render({ canvasContext: origCtx, viewport: viewport }).promise;
253+
254+
// Setup Converted Canvas
255+
const prevCanvas = document.getElementById('previewCanvas');
256+
const prevCtx = prevCanvas.getContext('2d', { willReadFrequently: true });
257+
prevCanvas.width = viewport.width;
258+
prevCanvas.height = viewport.height;
259+
260+
// Draw original onto preview canvas, then apply tint
261+
prevCtx.drawImage(origCanvas, 0, 0);
262+
applyColorTint(prevCtx, prevCanvas.width, prevCanvas.height, targetColor, forceMonochrome);
263+
264+
// Update Pagination UI
265+
document.getElementById('pageIndicator').innerText = `Page ${pageNum} of ${loadedPdf.numPages}`;
266+
document.getElementById('prevPageBtn').disabled = pageNum <= 1;
267+
document.getElementById('nextPageBtn').disabled = pageNum >= loadedPdf.numPages;
268+
269+
} catch (err) {
270+
console.error(err);
271+
statusDiv.innerText = "Error rendering page.";
272+
}
273+
}
274+
275+
// Load PDF and initialize preview
276+
document.getElementById('previewBtn').addEventListener('click', async () => {
277+
const fileInput = document.getElementById('pdfInput');
278+
const statusDiv = document.getElementById('status');
279+
280+
if (!fileInput.files.length) {
281+
statusDiv.innerText = "Please select a PDF file first.";
282+
return;
283+
}
284+
285+
const file = fileInput.files[0];
286+
originalFileName = file.name.replace('.pdf', '');
287+
statusDiv.innerText = "Loading PDF...";
288+
289+
const fileReader = new FileReader();
290+
fileReader.onload = async function() {
291+
try {
292+
const typedarray = new Uint8Array(this.result);
293+
loadedPdf = await pdfjsLib.getDocument({ data: typedarray }).promise;
294+
currentPage = 1;
295+
296+
await renderPageToUI(currentPage);
297+
298+
document.getElementById('workspace').style.display = 'block';
299+
document.getElementById('downloadBtn').disabled = false;
300+
statusDiv.innerText = "Preview generated! Adjust settings or download.";
301+
302+
} catch (error) {
303+
console.error(error);
304+
statusDiv.innerText = "Error loading PDF.";
305+
}
306+
};
307+
fileReader.readAsArrayBuffer(file);
308+
});
309+
310+
// Pagination Controls
311+
document.getElementById('prevPageBtn').addEventListener('click', () => {
312+
if (currentPage > 1) {
313+
currentPage--;
314+
renderPageToUI(currentPage);
315+
}
316+
});
317+
318+
document.getElementById('nextPageBtn').addEventListener('click', () => {
319+
if (currentPage < loadedPdf.numPages) {
320+
currentPage++;
321+
renderPageToUI(currentPage);
322+
}
323+
});
324+
325+
// Download Full PDF
326+
document.getElementById('downloadBtn').addEventListener('click', async () => {
327+
if (!loadedPdf) return;
328+
329+
const statusDiv = document.getElementById('status');
330+
const targetColor = document.getElementById('colorSelect').value;
331+
const forceMonochrome = document.getElementById('forceGrayscale').checked;
332+
const scale = parseFloat(document.getElementById('pdfScale').value);
333+
const quality = parseFloat(document.getElementById('pdfQuality').value);
334+
335+
const { jsPDF } = window.jspdf;
336+
337+
document.getElementById('downloadBtn').disabled = true;
338+
document.getElementById('previewBtn').disabled = true;
339+
340+
try {
341+
let newPdf = null;
342+
343+
for (let pageNum = 1; pageNum <= loadedPdf.numPages; pageNum++) {
344+
statusDiv.innerText = `Processing page ${pageNum} of ${loadedPdf.numPages}... (High Quality)`;
345+
346+
const page = await loadedPdf.getPage(pageNum);
347+
const viewport = page.getViewport({ scale: scale });
348+
349+
const canvas = document.createElement('canvas');
350+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
351+
canvas.width = viewport.width;
352+
canvas.height = viewport.height;
353+
354+
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
355+
356+
applyColorTint(ctx, canvas.width, canvas.height, targetColor, forceMonochrome);
357+
358+
const pdfWidth = viewport.width / scale;
359+
const pdfHeight = viewport.height / scale;
360+
361+
if (pageNum === 1) {
362+
newPdf = new jsPDF({
363+
orientation: pdfWidth > pdfHeight ? 'landscape' : 'portrait',
364+
unit: 'pt',
365+
format: [pdfWidth, pdfHeight]
366+
});
367+
} else {
368+
newPdf.addPage([pdfWidth, pdfHeight], pdfWidth > pdfHeight ? 'landscape' : 'portrait');
369+
}
370+
371+
const imgDataUrl = canvas.toDataURL('image/jpeg', quality);
372+
newPdf.addImage(imgDataUrl, 'JPEG', 0, 0, pdfWidth, pdfHeight);
373+
}
374+
375+
statusDiv.innerText = "Done! Check your downloads folder.";
376+
377+
const colorSelect = document.getElementById('colorSelect');
378+
const colorName = colorSelect.options[colorSelect.selectedIndex].text.split(' ')[1];
379+
newPdf.save(`${originalFileName}_${colorName}_Printable.pdf`);
380+
381+
} catch (error) {
382+
console.error(error);
383+
statusDiv.innerText = "An error occurred during download.";
384+
} finally {
385+
document.getElementById('downloadBtn').disabled = false;
386+
document.getElementById('previewBtn').disabled = false;
387+
}
388+
});
389+
</script>
390+
391+
</body>
392+
</html>

0 commit comments

Comments
 (0)