Skip to content

Commit ecf769d

Browse files
committed
Optimizer: Add granular image processing controls and improve image handling. Issue #3
1 parent c1b3562 commit ecf769d

File tree

6 files changed

+79
-9
lines changed

6 files changed

+79
-9
lines changed

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@ A tool for converting EPUB files to XTC/XTCH format and optimizing EPUBs for e-i
2626
- Optimize EPUB files for e-ink readers
2727
- Remove problematic CSS (floats, flex, grid, fixed positioning)
2828
- Strip embedded fonts to reduce file size
29-
- Convert images to grayscale
30-
- Resize images to configurable max width
29+
- Image processing (toggleable):
30+
- Convert images to grayscale
31+
- Resize images to configurable max width/height
32+
- Flatten alpha transparency to white background
33+
- Skip tiny decorative images (<20px)
34+
- Only replace images when processed version is smaller
35+
- Remove unsupported image formats (SVG, WebP, TIFF)
3136
- Inject e-paper optimized CSS
3237
- Batch processing with ZIP export
3338

@@ -57,7 +62,8 @@ A tool for converting EPUB files to XTC/XTCH format and optimizing EPUBs for e-i
5762

5863
### Optimizer Tab
5964
- Drop EPUBs and switch to the Optimizer tab
60-
- Configure optimization options
65+
- Configure optimization options (CSS removal, font stripping, image processing, unsupported format removal, CSS injection)
66+
- Image sub-controls (grayscale, max width, unsupported format removal) are disabled when "Process images" is unchecked
6167
- Click "Optimize EPUBs" to download optimized files
6268

6369
### CLI Usage
@@ -87,9 +93,14 @@ node index.js optimize book.epub -o book_optimized.epub -c settings.json
8793

8894
# Optimize all EPUBs in a directory
8995
node index.js optimize ./epubs/ -o ./output/ -c settings.json
96+
97+
# Optimize recursively (set "recursive": true in settings.json optimizer section)
98+
node index.js optimize ./library/ -o ./optimized/ -c settings.json
9099
```
91100

92-
Optimization options are configured in `settings.json` under the `optimizer` section. Set `recursive` to `true` to process subdirectories; use `include`/`exclude` glob patterns to filter files.
101+
Optimization options are configured in `settings.json` under the `optimizer` section:
102+
- Set `recursive` to `true` to process subdirectories (preserves directory structure in output)
103+
- Use `include`/`exclude` glob patterns to filter files (e.g., `"exclude": "*_optimized.epub"`)
93104

94105
Example `settings.json`:
95106
```json
@@ -104,6 +115,8 @@ Example `settings.json`:
104115
"optimizer": {
105116
"removeCss": true,
106117
"stripFonts": true,
118+
"processImages": true,
119+
"removeUnsupportedImages": true,
107120
"grayscale": true,
108121
"maxImageWidth": 480,
109122
"injectCss": true,

cli/optimizer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ async function optimizeEpub(inputPath, outputPath, options) {
226226
}
227227

228228
// Remove unsupported image formats (GIF, SVG, WebP, TIFF)
229-
if (/\.(gif|svg|webp|tiff?)$/i.test(filePath)) {
229+
if (options.removeUnsupportedImages && /\.(gif|svg|webp|tiff?)$/i.test(filePath)) {
230230
// Try to convert to JPEG via sharp, remove if conversion fails
231231
try {
232232
const imgData = await zipFile.async('nodebuffer');
@@ -280,7 +280,7 @@ async function optimizeEpub(inputPath, outputPath, options) {
280280
}
281281

282282
// Process supported images (JPEG, PNG, BMP)
283-
if ((options.grayscale || options.maxImageWidth) && /\.(jpg|jpeg|png|bmp)$/i.test(filePath)) {
283+
if (options.processImages && /\.(jpg|jpeg|png|bmp)$/i.test(filePath)) {
284284
const imgData = await zipFile.async('nodebuffer');
285285
const processed = await processImage(imgData, options.maxImageWidth, options.grayscale);
286286
if (processed && processed.length < imgData.length) {

cli/settings.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ const DEFAULT_SETTINGS = {
5151
optimizer: {
5252
removeCss: true,
5353
stripFonts: true,
54+
processImages: true,
55+
removeUnsupportedImages: true,
5456
grayscale: true,
5557
maxImageWidth: 480,
5658
injectCss: true,
57-
recursive: false,
59+
recursive: true,
5860
include: '*.epub',
5961
exclude: null
6062
}

web/app.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,23 @@ function setupSettings() {
813813
document.getElementById('maxImageWidthNum'),
814814
'maxImageWidthValue');
815815

816+
// Toggle dependent image controls when processImages changes
817+
var optProcessImages = document.getElementById('optProcessImages');
818+
var imageSubControls = [
819+
document.getElementById('optGrayscale'),
820+
document.getElementById('optRemoveUnsupported'),
821+
document.getElementById('maxImageWidth'),
822+
document.getElementById('maxImageWidthNum')
823+
];
824+
function updateImageSubControls() {
825+
var enabled = optProcessImages.checked;
826+
for (var i = 0; i < imageSubControls.length; i++) {
827+
imageSubControls[i].disabled = !enabled;
828+
}
829+
}
830+
optProcessImages.addEventListener('change', updateImageSubControls);
831+
updateImageSubControls();
832+
816833
// Status bar sliders (render-only, no applySettings needed)
817834
syncInputsRenderOnly(statusFontSize, statusFontSizeNum, 'statusFontSizeValue');
818835
syncInputsRenderOnly(statusEdgeMargin, statusEdgeMarginNum, 'statusEdgeMarginValue');
@@ -1744,6 +1761,8 @@ async function optimizeEpub(file) {
17441761
var settings = {
17451762
removeCss: document.getElementById('optRemoveCss').checked,
17461763
stripFonts: document.getElementById('optStripFonts').checked,
1764+
processImages: document.getElementById('optProcessImages').checked,
1765+
removeUnsupportedImages: document.getElementById('optRemoveUnsupported').checked,
17471766
grayscale: document.getElementById('optGrayscale').checked,
17481767
maxWidth: parseInt(document.getElementById('maxImageWidth').value),
17491768
injectCss: document.getElementById('optInjectCss').checked
@@ -1785,11 +1804,17 @@ async function optimizeEpub(file) {
17851804
epubZip.file(path, html);
17861805
}
17871806

1807+
// Remove unsupported image formats (can't reliably process in browser, won't display on device)
1808+
if (settings.processImages && settings.removeUnsupportedImages && /\.(svg|webp|tiff?)$/i.test(path)) {
1809+
epubZip.remove(path);
1810+
continue;
1811+
}
1812+
17881813
// Process images
1789-
if (settings.grayscale && /\.(jpg|jpeg|png|gif)$/i.test(path)) {
1814+
if (settings.processImages && /\.(jpg|jpeg|png|bmp|gif)$/i.test(path)) {
17901815
var imgData = await zipFile.async('arraybuffer');
17911816
var processedImg = await processImage(imgData, settings.maxWidth, settings.grayscale);
1792-
if (processedImg) {
1817+
if (processedImg && processedImg.byteLength < imgData.byteLength) {
17931818
epubZip.file(path, processedImg);
17941819
}
17951820
}
@@ -1847,6 +1872,9 @@ async function processImage(imgData, maxWidth, toGrayscale) {
18471872
var blob = new Blob([imgData]);
18481873
var img = new Image();
18491874
img.onload = function() {
1875+
// Skip tiny decorative images
1876+
if (img.width < 20 || img.height < 20) { resolve(null); return; }
1877+
18501878
var canvas = document.createElement('canvas');
18511879
var ctx = canvas.getContext('2d');
18521880

@@ -1859,8 +1887,20 @@ async function processImage(imgData, maxWidth, toGrayscale) {
18591887
width = maxWidth;
18601888
}
18611889

1890+
// Constrain height to device decode limit
1891+
var maxHeight = 3072;
1892+
if (height > maxHeight) {
1893+
width = Math.round(width * (maxHeight / height));
1894+
height = maxHeight;
1895+
}
1896+
18621897
canvas.width = width;
18631898
canvas.height = height;
1899+
1900+
// Flatten alpha to white background (matches CLI behavior)
1901+
ctx.fillStyle = '#ffffff';
1902+
ctx.fillRect(0, 0, width, height);
1903+
18641904
ctx.drawImage(img, 0, 0, width, height);
18651905

18661906
// Convert to grayscale if enabled

web/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,21 @@ <h2>EPUB Optimizer</h2>
318318
<label for="optStripFonts">Strip embedded fonts</label>
319319
</div>
320320

321+
<div class="checkbox-group">
322+
<input type="checkbox" id="optProcessImages" checked>
323+
<label for="optProcessImages">Process images (resize & convert to baseline JPEG)</label>
324+
</div>
325+
321326
<div class="checkbox-group">
322327
<input type="checkbox" id="optGrayscale" checked>
323328
<label for="optGrayscale">Convert images to grayscale</label>
324329
</div>
325330

331+
<div class="checkbox-group">
332+
<input type="checkbox" id="optRemoveUnsupported" checked>
333+
<label for="optRemoveUnsupported">Remove unsupported image formats (SVG, WebP, TIFF)</label>
334+
</div>
335+
326336
<div class="control-group">
327337
<label>Max Image Width: <span id="maxImageWidthValue">480</span>px</label>
328338
<div class="slider-row">

web/style.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ input[type="range"]::-webkit-slider-thumb {
215215
margin: 0;
216216
}
217217

218+
.checkbox-group input[type="checkbox"]:disabled + label,
219+
.control-group:has(input:disabled) label {
220+
opacity: 0.4;
221+
}
222+
218223
/* Checkbox row for multiple checkboxes */
219224
.checkbox-row {
220225
display: flex;

0 commit comments

Comments
 (0)