A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: integrated epub optimizer (#1224)

## Problem

Many e-ink readers have limited image decoder support natively.
EPUBs with images in other formats than **baseline JPEG** frequently
cause:

- **Broken images**: pages render as blank, corrupted noise, or never
load
- **Slow rendering**: unoptimized images cause severe delays on e-ink
hardware, up to 7 seconds per page turn, with cover images taking up to
59 seconds to render
- **Broken covers**: the book thumbnail never generates

Fixing this today requires external tools before uploading.

---

## What this PR does

Adds an **optional, on-demand EPUB optimizer** to the file upload flow.
When enabled,
it converts all images to baseline JPEG directly in the browser — no
server, no internet,
no external tools needed.

**Conversion is opt-in. The standard upload flow is unchanged.**

---

## Real-world impact

The optimizer was applied in batch to **61 EPUBs**:
- 60 standard EPUBs: 198 MB → 55 MB (**−72.2%**, 143 MB saved)
- Text-dominant books: 8–46% smaller (covers and inline images
converted)
- Image-heavy / illustrated books: 65–93% smaller
- 1 Large manga volume (594 MB): 594 MB → 72 MB (**−87.8%**, 522 MB
saved)
- EPUB structural integrity fully maintained — zero new validation
issues introduced across all 61 books

*Size and integrity analysis:
[epub-comparator](https://github.com/pablohc/epub-comparator)*

From that set, **17 books were selected** as a representative sample
covering different content
types: image-heavy novels, pure manga, light novels with broken images,
and text-dominant books.
Each was benchmarked on two devices running in parallel, one on `master`
and one
on `PR#1224` — measuring render time across ~30 pages per book on
average.

### Rendering bugs fixed

| Book | Problem (original) | After optimization |
|------|--------------------|--------------------|
| Fairy Tale — Stephen King | Cover took **59.7 s** to render | 2.1 s
(−96%) |
| Cycle of the Werewolf — Stephen King | Cover took **23.3 s** to render
| 1.7 s (−93%) |
| Tomie: Complete Deluxe Ed. — Junji Ito | Cover took **18.3 s** to
render | 2.0 s (−89%) |
| Joel Dicker — El tigre (Ed. Ilustrada) | Cover took **14.5 s** to
render | 1.4 s (−90%) |
| Jackson, Holly — Asesinato para principiantes | Cover failed
completely (blank) | 2.0 s ✓ |
| Sentenced to Be a Hero — Yen Press | Cover failed, **8 images failed
to load** | All fixed ✓ |
| Flynn, Gillian — Perdida | Cover failed completely (blank) | 1.6 s ✓ |
| Chandler, Raymond — Asesino en la lluvia | Cover failed completely
(blank) | 2.0 s ✓ |

### Page render times — image-heavy EPUBs (avg per page)

| Book | Pages | Avg original | Avg optimized | Improvement | File size
|
|------|-------|-------------|---------------|-------------|-----------|
| Fairy Tale — Stephen King | 30 | 3,028 ms | 1,066 ms | **−64.8%** |
32.4 MB → 9.1 MB (−72%) |
| Cycle of the Werewolf — Stephen King | 33 | 3,026 ms | 1,558 ms |
**−48.5%** | 35.1 MB → 2.9 MB (−92%) |
| Joel Dicker — El tigre (Ed. Ilustrada) | 16 | 1,846 ms | 1,051 ms |
**−43.1%** | 5.3 MB → 0.4 MB (−93%) |
| Tomie: Complete Deluxe Ed. — Junji Ito | 30 | 4,817 ms | 2,802 ms |
**−41.8%** | 593.8 MB → 72.2 MB (−87.8%) |
| Sentenced to Be a Hero — Yen Press | 30 | 1,719 ms | 1,388 ms |
**−19.2%** | 15.2 MB → 1.6 MB (−90%) |

### Text-heavy EPUBs — no regression

| Book | Pages | Avg original | Avg optimized | Delta |
|------|-------|-------------|---------------|-------|
| Christie — Asesinato en el Orient Express | 30 | 1,672 ms | 1,646 ms |
−1.6% |
| Flynn — Perdida | 30 | 1,327 ms | 1,291 ms | −2.7% |
| Dicker — La verdad sobre el caso Harry Quebert | 30 | 1,132 ms | 1,084
ms | −4.2% |
| Hammett — El halcón maltés | 30 | 1,009 ms | 966 ms | −4.3% |
| Chandler — Asesino en la lluvia | 30 | 989 ms | 1,007 ms | +1.8% |

*Differences within ±5% — consistent with device measurement noise.*

*Render time benchmark:
[epub-optimization-benchmark](https://github.com/pablohc/epub-optimization-benchmark)*

---
## How to use it

**Single file:**
1. Click **Upload** (top of the page) — a modal opens. Use **Choose
files** to select one EPUB from your device.
2. Check **Optimize**.
- *(Optional)* Expand **Advanced Mode** — adjust quality, rotation, or
overlap; set individual images to H-Split / V-Split / Rotate.
3. Click **Optimize & Upload**.

**Batch (2+ files):**
1. Click **Upload** (top of the page) — a modal opens. Use **Choose
files** to select multiple EPUBs from your device.
2. Check **Optimize**.
- *(Optional)* Expand **Advanced Mode** — adjust quality.
3. Click **Upload** — all files are converted and uploaded sequentially.

Upload a batch of files, without optimization:
<img width="810" height="671" alt="image"
src="https://github.com/user-attachments/assets/d892ae13-0b87-4ea4-b6b8-340d56efc763"
/>

Batch file upload, with standard optimization:
<img width="809" height="707" alt="image"
src="https://github.com/user-attachments/assets/d32dbc88-1208-4555-bfcf-330ab91d2174"
/>

Optimization Phase (1/2):
<img width="807" height="1055" alt="image"
src="https://github.com/user-attachments/assets/fd4cd5f9-e56e-4ca1-9777-6926b9baf2bb"
/>

Upload Phase (2/2):
<img width="805" height="1065" alt="image"
src="https://github.com/user-attachments/assets/483294f0-02f0-4569-ae11-c10b3581d747"
/>

Batch upload successfully confirmed:
<img width="812" height="1043" alt="image"
src="https://github.com/user-attachments/assets/80c135bf-05c3-4c80-8755-2a04c68235bc"
/>

---

## Options

**Always active when the converter is enabled:**
- Converts PNG, WebP, BMP, GIF → baseline JPEG
- Smart downscaling to 480×800 px max (preserves aspect ratio)
- True grayscale for e-ink (BT.709 luminance, always on)
- SVG cover fix + OPF/NCX compliance repairs

**Advanced Mode (opt-in) — single file:**
- JPEG quality presets: 30% / 45% / 60% / 75% / **85%** (default) / 95%
- Rotation direction for split images: CW (default) / CCW
- Min overlap when splitting: 5% (default) / 10% / 15%
- Auto-download conversion log toggle (detailed stats per image)
- Per-image picker: set Normal / H-Split / V-Split / Rotate per image
individually,
with "Apply to all" for bulk assignment

**Advanced Mode (opt-in) — batch (2+ files):**
- JPEG quality presets: 30% / 45% / 60% / 75% / **85%** (default) / 95%
- Auto-download conversion log toggle (aggregated stats for all files)

---

## ⚠️ Known limitations

**KoReader hash-based sync will break** for converted files. The file
content changes,
so the hash no longer matches the original. Filename-based sync is
unaffected.

If you rely on KoReader hash sync, use the Calibre plugin or the web
tool instead.

---
## Build size impact

| Metric | master (53beeee) | PR #1224 (a2ba5db) | Delta |

|---------------|------------------|--------------------|----------------|
| Flash used | 5,557 KB | 5,616 KB | +59 KB (+1.1%) |
| Flash free | 843 KB | 784 KB | −59 KB |
| Flash usage | 86.8% | 87.7% | +0.9 pp |
| RAM used | 95,156 B | 95,156 B | no change |

> Both builds compiled with `gh_release` environment in release mode
(ESP32-C3, 6,400 KB Flash).
> The +59 KB increase is entirely due to `jszip.min.js` embedded as a
> gzipped static asset served from Flash. RAM usage is identical,
> confirming no runtime overhead — the library runs in the browser,
> not on the ESP32. ~784 KB of Flash remain available.

---

## Alternatives considered

| Approach | Friction |
|----------|---------|
| **This PR** — integrated in upload flow | Zero: convert + upload in
one step, offline, any browser |
| Calibre plugin (in parallel development) | Requires a computer with
Calibre installed, same network |
| Web converters | Requires extra upload / download / transfer steps |

---

## Credits

Based on the converter algorithm developed by @zgredex.
Co-authored-by: @zgredex

---

### AI Usage

Did you use AI tools to help write this code? **PARTIALLY**

---------

Co-authored-by: zgredex <zgredex@users.noreply.github.com>

authored by

pablohc
zgredex
and committed by
GitHub
7d56810e 526c8a5e

+3851 -83
+32 -13
scripts/build_html.py
··· 32 32 33 33 return html.strip() 34 34 35 + def sanitize_identifier(name: str) -> str: 36 + """Sanitize a filename to create a valid C identifier. 37 + 38 + C identifiers must: 39 + - Start with a letter or underscore 40 + - Contain only letters, digits, and underscores 41 + """ 42 + # Replace non-alphanumeric characters (including hyphens) with underscores 43 + sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', name) 44 + # Prefix with underscore if starts with a digit 45 + if sanitized and sanitized[0].isdigit(): 46 + sanitized = f"_{sanitized}" 47 + return sanitized 48 + 35 49 for root, _, files in os.walk(SRC_DIR): 36 50 for file in files: 37 - if file.endswith(".html"): 38 - html_path = os.path.join(root, file) 39 - with open(html_path, "r", encoding="utf-8") as f: 40 - html_content = f.read() 51 + if file.endswith(".html") or file.endswith(".js"): 52 + file_path = os.path.join(root, file) 53 + with open(file_path, "r", encoding="utf-8") as f: 54 + content = f.read() 41 55 42 - # minified = regex.sub("\g<1>", html_content) 43 - minified = minify_html(html_content) 56 + # Only minify HTML files; JS files are typically pre-minified (e.g., jszip.min.js) 57 + if file.endswith(".html"): 58 + processed = minify_html(content) 59 + else: 60 + processed = content 44 61 45 62 # Compress with gzip (compresslevel 9 is maximum compression) 46 63 # IMPORTANT: we don't use brotli because Firefox doesn't support brotli with insecured context (only supported on HTTPS) 47 - compressed = gzip.compress(minified.encode('utf-8'), compresslevel=9) 64 + compressed = gzip.compress(processed.encode('utf-8'), compresslevel=9) 48 65 49 - base_name = f"{os.path.splitext(file)[0]}Html" 66 + # Create valid C identifier from filename 67 + # Use appropriate suffix based on file type 68 + suffix = "Html" if file.endswith(".html") else "Js" 69 + base_name = sanitize_identifier(f"{os.path.splitext(file)[0]}{suffix}") 50 70 header_path = os.path.join(root, f"{base_name}.generated.h") 51 71 52 72 with open(header_path, "w", encoding="utf-8") as h: ··· 65 85 66 86 h.write(f"}};\n\n") 67 87 h.write(f"constexpr size_t {base_name}CompressedSize = {len(compressed)};\n") 68 - h.write(f"constexpr size_t {base_name}OriginalSize = {len(minified)};\n") 88 + h.write(f"constexpr size_t {base_name}OriginalSize = {len(processed)};\n") 69 89 70 90 print(f"Generated: {header_path}") 71 - print(f" Original: {len(html_content)} bytes") 72 - print(f" Minified: {len(minified)} bytes ({100*len(minified)/len(html_content):.1f}%)") 73 - print(f" Compressed: {len(compressed)} bytes ({100*len(compressed)/len(html_content):.1f}%)") 74 - 91 + print(f" Original: {len(content)} bytes") 92 + print(f" Minified: {len(processed)} bytes ({100*len(processed)/len(content):.1f}%)") 93 + print(f" Compressed: {len(compressed)} bytes ({100*len(compressed)/len(content):.1f}%)")
+102 -24
src/network/CrossPointWebServer.cpp
··· 16 16 #include "html/FilesPageHtml.generated.h" 17 17 #include "html/HomePageHtml.generated.h" 18 18 #include "html/SettingsPageHtml.generated.h" 19 + #include "html/js/jszip_minJs.generated.h" 19 20 20 21 namespace { 21 22 // Folders/files to hide from the web interface file browser ··· 36 37 size_t wsUploadReceived = 0; 37 38 unsigned long wsUploadStartTime = 0; 38 39 bool wsUploadInProgress = false; 40 + uint8_t wsUploadClientNum = 255; // 255 = no active upload client 41 + size_t wsLastProgressSent = 0; 39 42 String wsLastCompleteName; 40 43 size_t wsLastCompleteSize = 0; 41 44 unsigned long wsLastCompleteAt = 0; ··· 131 134 LOG_DBG("WEB", "Setting up routes..."); 132 135 server->on("/", HTTP_GET, [this] { handleRoot(); }); 133 136 server->on("/files", HTTP_GET, [this] { handleFileList(); }); 137 + server->on("/js/jszip.min.js", HTTP_GET, [this] { handleJszip(); }); 134 138 135 139 server->on("/api/status", HTTP_GET, [this] { handleStatus(); }); 136 140 server->on("/api/files", HTTP_GET, [this] { handleFileListData(); }); ··· 188 192 LOG_DBG("WEB", "[MEM] Free heap after server.begin(): %d bytes", ESP.getFreeHeap()); 189 193 } 190 194 195 + void CrossPointWebServer::abortWsUpload(const char* tag) { 196 + wsUploadFile.close(); 197 + String filePath = wsUploadPath; 198 + if (!filePath.endsWith("/")) filePath += "/"; 199 + filePath += wsUploadFileName; 200 + if (Storage.remove(filePath.c_str())) { 201 + LOG_DBG(tag, "Deleted incomplete upload: %s", filePath.c_str()); 202 + } else { 203 + LOG_DBG(tag, "Failed to delete incomplete upload: %s", filePath.c_str()); 204 + } 205 + wsUploadInProgress = false; 206 + wsUploadClientNum = 255; 207 + wsLastProgressSent = 0; 208 + } 209 + 191 210 void CrossPointWebServer::stop() { 192 211 if (!running || !server) { 193 212 LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get()); ··· 199 218 200 219 LOG_DBG("WEB", "[MEM] Free heap before stop: %d bytes", ESP.getFreeHeap()); 201 220 202 - // Close any in-progress WebSocket upload 221 + // Close any in-progress WebSocket upload and remove partial file 203 222 if (wsUploadInProgress && wsUploadFile) { 204 - wsUploadFile.close(); 205 - wsUploadInProgress = false; 223 + abortWsUpload("WEB"); 206 224 } 207 225 208 226 // Stop WebSocket server ··· 307 325 void CrossPointWebServer::handleRoot() const { 308 326 sendHtmlContent(server.get(), HomePageHtml, sizeof(HomePageHtml)); 309 327 LOG_DBG("WEB", "Served root page"); 328 + } 329 + 330 + void CrossPointWebServer::handleJszip() const { 331 + server->sendHeader("Content-Encoding", "gzip"); 332 + server->send_P(200, "application/javascript", jszip_minJs, jszip_minJsCompressedSize); 333 + LOG_DBG("WEB", "Served jszip.min.js"); 310 334 } 311 335 312 336 void CrossPointWebServer::handleNotFound() const { ··· 505 529 server->send(200, contentType.c_str(), ""); 506 530 507 531 NetworkClient client = server->client(); 508 - client.write(file); 532 + const size_t chunkSize = 4096; 533 + uint8_t buffer[chunkSize]; 534 + 535 + bool downloadOk = true; 536 + while (downloadOk && file.available()) { 537 + int result = file.read(buffer, chunkSize); 538 + if (result <= 0) break; 539 + size_t bytesRead = static_cast<size_t>(result); 540 + size_t totalWritten = 0; 541 + while (totalWritten < bytesRead) { 542 + esp_task_wdt_reset(); 543 + size_t wrote = client.write(buffer + totalWritten, bytesRead - totalWritten); 544 + if (wrote == 0) { 545 + downloadOk = false; 546 + break; 547 + } 548 + totalWritten += wrote; 549 + } 550 + } 551 + client.clear(); 509 552 file.close(); 510 553 } 511 554 ··· 1082 1125 doc["type"] = "string"; 1083 1126 if (s.stringGetter) { 1084 1127 doc["value"] = s.stringGetter(); 1085 - } else if (s.stringOffset > 0) { 1128 + } else if (s.stringMaxLen > 0) { 1086 1129 doc["value"] = reinterpret_cast<const char*>(&SETTINGS) + s.stringOffset; 1087 1130 } 1088 1131 break; ··· 1166 1209 const std::string val = doc[s.key].as<std::string>(); 1167 1210 if (s.stringSetter) { 1168 1211 s.stringSetter(val); 1169 - } else if (s.stringOffset > 0 && s.stringMaxLen > 0) { 1212 + } else if (s.stringMaxLen > 0) { 1170 1213 char* ptr = reinterpret_cast<char*>(&SETTINGS) + s.stringOffset; 1171 1214 strncpy(ptr, val.c_str(), s.stringMaxLen - 1); 1172 1215 ptr[s.stringMaxLen - 1] = '\0'; ··· 1202 1245 switch (type) { 1203 1246 case WStype_DISCONNECTED: 1204 1247 LOG_DBG("WS", "Client %u disconnected", num); 1205 - // Clean up any in-progress upload 1206 - if (wsUploadInProgress && wsUploadFile) { 1207 - wsUploadFile.close(); 1208 - // Delete incomplete file 1209 - String filePath = wsUploadPath; 1210 - if (!filePath.endsWith("/")) filePath += "/"; 1211 - filePath += wsUploadFileName; 1212 - Storage.remove(filePath.c_str()); 1213 - LOG_DBG("WS", "Deleted incomplete upload: %s", filePath.c_str()); 1248 + // Only clean up if this is the client that owns the active upload. 1249 + // A new client may have already started a fresh upload before this 1250 + // DISCONNECTED event fires (race condition on quick cancel + retry). 1251 + if (num == wsUploadClientNum && wsUploadInProgress && wsUploadFile) { 1252 + abortWsUpload("WS"); 1214 1253 } 1215 - wsUploadInProgress = false; 1216 1254 break; 1217 1255 1218 1256 case WStype_CONNECTED: { ··· 1226 1264 LOG_DBG("WS", "Text from client %u: %s", num, msg.c_str()); 1227 1265 1228 1266 if (msg.startsWith("START:")) { 1267 + // Reject any START while an upload is already active to prevent 1268 + // leaking the open wsUploadFile handle (owning client re-START included) 1269 + if (wsUploadInProgress) { 1270 + wsServer->sendTXT(num, "ERROR:Upload already in progress"); 1271 + break; 1272 + } 1273 + 1229 1274 // Parse: START:<filename>:<size>:<path> 1230 1275 int firstColon = msg.indexOf(':', 6); 1231 1276 int secondColon = msg.indexOf(':', firstColon + 1); 1232 1277 1233 1278 if (firstColon > 0 && secondColon > 0) { 1234 1279 wsUploadFileName = msg.substring(6, firstColon); 1235 - wsUploadSize = msg.substring(firstColon + 1, secondColon).toInt(); 1280 + String sizeToken = msg.substring(firstColon + 1, secondColon); 1281 + bool sizeValid = sizeToken.length() > 0; 1282 + int digitStart = (sizeValid && sizeToken[0] == '+') ? 1 : 0; 1283 + if (digitStart > 0 && sizeToken.length() < 2) sizeValid = false; 1284 + for (int i = digitStart; i < (int)sizeToken.length() && sizeValid; i++) { 1285 + if (!isdigit((unsigned char)sizeToken[i])) sizeValid = false; 1286 + } 1287 + if (!sizeValid) { 1288 + LOG_DBG("WS", "START rejected: invalid size token '%s'", sizeToken.c_str()); 1289 + wsServer->sendTXT(num, "ERROR:Invalid START format"); 1290 + return; 1291 + } 1292 + wsUploadSize = sizeToken.toInt(); 1236 1293 wsUploadPath = msg.substring(secondColon + 1); 1237 1294 wsUploadReceived = 0; 1295 + wsLastProgressSent = 0; 1238 1296 wsUploadStartTime = millis(); 1239 1297 1240 1298 // Ensure path is valid ··· 1262 1320 if (!Storage.openFileForWrite("WS", filePath, wsUploadFile)) { 1263 1321 wsServer->sendTXT(num, "ERROR:Failed to create file"); 1264 1322 wsUploadInProgress = false; 1323 + wsUploadClientNum = 255; 1265 1324 return; 1266 1325 } 1267 1326 esp_task_wdt_reset(); 1268 1327 1328 + // Zero-byte upload: complete immediately without waiting for BIN frames 1329 + if (wsUploadSize == 0) { 1330 + wsUploadFile.close(); 1331 + wsLastCompleteName = wsUploadFileName; 1332 + wsLastCompleteSize = 0; 1333 + wsLastCompleteAt = millis(); 1334 + LOG_DBG("WS", "Zero-byte upload complete: %s", filePath.c_str()); 1335 + clearEpubCacheIfNeeded(filePath); 1336 + wsServer->sendTXT(num, "DONE"); 1337 + wsLastProgressSent = 0; 1338 + break; 1339 + } 1340 + 1341 + wsUploadClientNum = num; 1269 1342 wsUploadInProgress = true; 1270 1343 wsServer->sendTXT(num, "READY"); 1271 1344 } else { ··· 1276 1349 } 1277 1350 1278 1351 case WStype_BIN: { 1279 - if (!wsUploadInProgress || !wsUploadFile) { 1352 + if (!wsUploadInProgress || !wsUploadFile || num != wsUploadClientNum) { 1280 1353 wsServer->sendTXT(num, "ERROR:No upload in progress"); 1281 1354 return; 1282 1355 } 1283 1356 1284 1357 // Write binary data directly to file 1358 + size_t remaining = wsUploadSize - wsUploadReceived; 1359 + if (length > remaining) { 1360 + abortWsUpload("WS"); 1361 + wsServer->sendTXT(num, "ERROR:Upload overflow"); 1362 + return; 1363 + } 1285 1364 esp_task_wdt_reset(); 1286 1365 size_t written = wsUploadFile.write(payload, length); 1287 1366 esp_task_wdt_reset(); 1288 1367 1289 1368 if (written != length) { 1290 - wsUploadFile.close(); 1291 - wsUploadInProgress = false; 1369 + abortWsUpload("WS"); 1292 1370 wsServer->sendTXT(num, "ERROR:Write failed - disk full?"); 1293 1371 return; 1294 1372 } ··· 1296 1374 wsUploadReceived += written; 1297 1375 1298 1376 // Send progress update (every 64KB or at end) 1299 - static size_t lastProgressSent = 0; 1300 - if (wsUploadReceived - lastProgressSent >= 65536 || wsUploadReceived >= wsUploadSize) { 1377 + if (wsUploadReceived - wsLastProgressSent >= 65536 || wsUploadReceived >= wsUploadSize) { 1301 1378 String progress = "PROGRESS:" + String(wsUploadReceived) + ":" + String(wsUploadSize); 1302 1379 wsServer->sendTXT(num, progress); 1303 - lastProgressSent = wsUploadReceived; 1380 + wsLastProgressSent = wsUploadReceived; 1304 1381 } 1305 1382 1306 1383 // Check if upload complete 1307 1384 if (wsUploadReceived >= wsUploadSize) { 1308 1385 wsUploadFile.close(); 1309 1386 wsUploadInProgress = false; 1387 + wsUploadClientNum = 255; 1310 1388 1311 1389 wsLastCompleteName = wsUploadFileName; 1312 1390 wsLastCompleteSize = wsUploadSize; ··· 1325 1403 clearEpubCacheIfNeeded(filePath); 1326 1404 1327 1405 wsServer->sendTXT(num, "DONE"); 1328 - lastProgressSent = 0; 1406 + wsLastProgressSent = 0; 1329 1407 } 1330 1408 break; 1331 1409 }
+2
src/network/CrossPointWebServer.h
··· 81 81 // WebSocket upload state 82 82 void onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length); 83 83 static void wsEventCallback(uint8_t num, WStype_t type, uint8_t* payload, size_t length); 84 + void abortWsUpload(const char* tag); 84 85 85 86 // File scanning 86 87 void scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const; ··· 89 90 90 91 // Request handlers 91 92 void handleRoot() const; 93 + void handleJszip() const; 92 94 void handleNotFound() const; 93 95 void handleStatus() const; 94 96 void handleFileList() const;
+3702 -46
src/network/html/FilesPage.html
··· 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <title>CrossPoint Reader - Files</title> 7 + <script src="/js/jszip.min.js"></script> 7 8 <style> 8 9 :root { 9 10 --font-color: #333; ··· 134 135 .folder-action-btn:hover { 135 136 background-color: #d68910; 136 137 } 138 + .delete-action-btn { 139 + background-color: #e74c3c; 140 + } 141 + .delete-action-btn:hover { 142 + background-color: #c0392b; 143 + } 137 144 /* Upload modal */ 138 145 .modal-overlay { 139 146 display: none; ··· 156 163 padding: 25px; 157 164 max-width: 450px; 158 165 width: 90%; 166 + max-height: 90vh; 167 + overflow-y: auto; 159 168 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); 160 169 } 170 + .modal.picker-mode { 171 + max-width: 920px; 172 + } 173 + .picker-columns.picker-active { 174 + display: flex; 175 + flex-direction: row; 176 + gap: 16px; 177 + align-items: flex-start; 178 + } 179 + .picker-columns.picker-active > .convert-options { 180 + flex: 0 0 340px; 181 + width: 340px; 182 + margin-bottom: 0; 183 + } 184 + .picker-columns.picker-active > .image-picker-section { 185 + flex: 1; 186 + margin-top: 0; 187 + max-height: 55vh; 188 + overflow-y: auto; 189 + } 190 + @media (max-width: 850px) { 191 + .modal.picker-mode { max-width: 95%; } 192 + .picker-columns.picker-active { 193 + flex-direction: column; 194 + } 195 + .picker-columns.picker-active > .convert-options { 196 + flex: 0 0 auto; 197 + width: 100%; 198 + } 199 + .picker-columns.picker-active > .image-picker-section { 200 + max-height: 40vh; 201 + } 202 + } 161 203 .modal h3 { 162 204 margin: 0 0 15px 0; 163 205 color: var(--title-color); ··· 170 212 cursor: pointer; 171 213 color: var(--label-color); 172 214 line-height: 1; 215 + transition: opacity 0.15s; 173 216 } 174 217 .modal-close:hover { 175 218 color: var(--title-color); 219 + } 220 + .modal-close.disabled { 221 + opacity: 0.3; 222 + cursor: not-allowed; 223 + } 224 + .modal-close.disabled:hover { 225 + color: var(--label-color); 176 226 } 177 227 .file-table { 178 228 width: 100%; ··· 224 274 text-decoration: underline; 225 275 } 226 276 .upload-form { 227 - margin-top: 10px; 277 + margin-bottom: 10px; 228 278 } 229 279 .upload-form input[type="file"] { 230 280 margin: 10px 0; 231 281 width: 100%; 232 282 } 283 + .upload-form input[type="file"]::file-selector-button { 284 + background-color: #27ae60; 285 + color: white; 286 + border: none; 287 + padding: 6px 14px; 288 + border-radius: 4px; 289 + cursor: pointer; 290 + font-size: 0.9em; 291 + margin-right: 8px; 292 + } 293 + .upload-form input[type="file"].has-files::file-selector-button { 294 + background-color: #95a5a6; 295 + } 233 296 .upload-btn { 234 297 background-color: #27ae60; 235 298 color: white; ··· 239 302 cursor: pointer; 240 303 font-size: 1em; 241 304 width: 100%; 305 + margin-top: 12px; 242 306 } 243 307 .upload-btn:hover { 244 308 background-color: #219a52; 245 309 } 310 + .upload-btn.optimize { 311 + background-color: #9b59b6; 312 + } 313 + .upload-btn.optimize:hover { 314 + background-color: #8e44ad; 315 + } 246 316 .upload-btn:disabled { 247 317 background-color: #95a5a6; 248 318 cursor: not-allowed; 249 319 } 320 + /* Convert options */ 321 + .convert-options { 322 + margin-bottom: 10px; 323 + padding: 12px; 324 + border-radius: 6px; 325 + border: 1px solid var(--accent-color); 326 + } 327 + .convert-checkbox { 328 + display: flex; 329 + align-items: center; 330 + gap: 8px; 331 + cursor: pointer; 332 + user-select: none; 333 + } 334 + .convert-checkbox input[type="checkbox"] { 335 + width: 18px; 336 + height: 18px; 337 + cursor: pointer; 338 + outline: none; 339 + } 340 + .convert-checkbox input[type="checkbox"]:focus { 341 + outline: none; 342 + box-shadow: none; 343 + } 344 + .convert-warning { 345 + font-size: 0.85em; 346 + color: #d68910; 347 + background-color: rgba(214, 137, 16, 0.1); 348 + border: 1px solid #d68910; 349 + border-radius: 4px; 350 + padding: 8px 12px; 351 + margin-top: 8px; 352 + margin-bottom: 12px; 353 + line-height: 1.4; 354 + text-align: center; 355 + } 356 + .convert-info { 357 + font-size: 0.85em; 358 + color: var(--label-color); 359 + margin-top: 8px; 360 + line-height: 1.4; 361 + } 362 + .convert-settings { 363 + font-size: 0.8em; 364 + color: var(--label-color); 365 + margin-top: 6px; 366 + padding-left: 26px; 367 + display: flex; 368 + flex-wrap: wrap; 369 + gap: 16px; 370 + } 371 + .convert-settings span { 372 + display: inline-block; 373 + } 374 + /* Convert row - compact layout with checkbox and advanced options toggle */ 375 + .convert-row { 376 + display: flex; 377 + align-items: center; 378 + justify-content: space-between; 379 + flex-wrap: wrap; 380 + } 381 + .convert-separator { 382 + display: none; /* Hidden, using space-between instead */ 383 + } 384 + .advanced-options-toggle { 385 + display: flex; 386 + align-items: center; 387 + gap: 4px; 388 + font-size: 0.85em; 389 + color: var(--label-color); 390 + cursor: pointer; 391 + user-select: none; 392 + transition: color 0.2s; 393 + } 394 + .advanced-options-toggle:hover { 395 + color: var(--accent-color); 396 + } 397 + /* Disable hover when disabled (opacity < 1) */ 398 + .advanced-options-toggle[style*="opacity: 0.3"]:hover, 399 + .advanced-options-toggle[style*="opacity: 0.5"]:hover { 400 + color: var(--label-color); 401 + } 402 + .advanced-options-text { 403 + font-weight: 500; 404 + } 405 + /* Advanced Options */ 406 + .advanced-options-arrow { 407 + font-size: 0.7em; 408 + transition: transform 0.2s; 409 + } 410 + .advanced-options-arrow.expanded { 411 + transform: rotate(90deg); 412 + } 413 + /* Settings content - hidden by default, shown when expanded */ 414 + .advanced-settings-content { 415 + display: none; 416 + margin-top: 12px; 417 + padding-left: 4px; 418 + } 419 + .advanced-settings-content.visible { 420 + display: block; 421 + } 422 + .advanced-setting-row { 423 + display: flex; 424 + justify-content: space-between; 425 + align-items: center; 426 + gap: 12px; 427 + margin-top: 12px; 428 + } 429 + .advanced-setting-row:first-child { 430 + margin-top: 10px; 431 + } 432 + .quality-row { 433 + flex-wrap: wrap; 434 + } 435 + .quality-row .setting-title { 436 + flex: 1; 437 + min-width: 100px; 438 + } 439 + .setting-label { 440 + flex-shrink: 0; 441 + } 442 + .setting-controls { 443 + display: flex; 444 + flex-direction: column; 445 + align-items: flex-end; 446 + gap: 8px; 447 + } 448 + .setting-row-header { 449 + display: flex; 450 + justify-content: space-between; 451 + align-items: center; 452 + margin-bottom: 8px; 453 + } 454 + .setting-title { 455 + font-size: 0.85em; 456 + color: var(--title-color); 457 + font-weight: 500; 458 + } 459 + .setting-value { 460 + font-size: 0.8em; 461 + color: var(--accent-color); 462 + font-weight: 600; 463 + } 464 + /* Quality Slider */ 465 + .quality-slider-container { 466 + display: flex; 467 + align-items: center; 468 + gap: 8px; 469 + margin-bottom: 8px; 470 + } 471 + .quality-slider { 472 + flex: 1; 473 + height: 6px; 474 + border-radius: 3px; 475 + background: var(--border-color); 476 + -webkit-appearance: none; 477 + appearance: none; 478 + cursor: pointer; 479 + } 480 + .quality-slider::-webkit-slider-thumb { 481 + -webkit-appearance: none; 482 + width: 16px; 483 + height: 16px; 484 + border-radius: 50%; 485 + background: var(--accent-color); 486 + cursor: pointer; 487 + } 488 + .quality-slider::-moz-range-thumb { 489 + width: 16px; 490 + height: 16px; 491 + border-radius: 50%; 492 + background: var(--accent-color); 493 + border: none; 494 + cursor: pointer; 495 + } 496 + .quality-input-wrapper { 497 + display: flex; 498 + align-items: center; 499 + background: var(--card-bg); 500 + border: 1px solid var(--border-color); 501 + border-radius: 4px; 502 + padding: 4px 8px; 503 + } 504 + .quality-input { 505 + width: 40px; 506 + border: none; 507 + background: transparent; 508 + text-align: center; 509 + font-size: 0.85em; 510 + color: var(--title-color); 511 + font-weight: 600; 512 + } 513 + .quality-input:focus { 514 + outline: none; 515 + } 516 + .quality-unit { 517 + font-size: 0.75em; 518 + color: var(--label-color); 519 + } 520 + .quality-slider-container-inline { 521 + display: flex; 522 + align-items: center; 523 + gap: 6px; 524 + flex: 1; 525 + max-width: 180px; 526 + min-width: 0; 527 + } 528 + .quality-slider-container-inline .quality-slider { 529 + flex: 1; 530 + min-width: 50px; 531 + } 532 + .quality-slider-container-inline .quality-input-wrapper { 533 + padding: 2px 4px; 534 + flex-shrink: 0; 535 + } 536 + .quality-slider-container-inline .quality-input { 537 + width: 32px; 538 + font-size: 0.8em; 539 + } 540 + /* Quality Presets - Compact Single Row */ 541 + .quality-presets { 542 + display: flex; 543 + gap: 0; 544 + background: var(--border-color); 545 + border-radius: 6px; 546 + padding: 3px; 547 + width: fit-content; 548 + } 549 + .quality-preset { 550 + padding: 6px 14px; 551 + background: transparent; 552 + border: none; 553 + border-radius: 4px; 554 + font-size: 0.75em; 555 + font-weight: 500; 556 + color: var(--label-color); 557 + text-align: center; 558 + cursor: pointer; 559 + transition: all 0.15s; 560 + white-space: nowrap; 561 + } 562 + .quality-preset:hover { 563 + background: var(--accent-color-10); 564 + color: var(--accent-color); 565 + } 566 + .quality-preset.active { 567 + background: var(--accent-color); 568 + color: white; 569 + box-shadow: 0 1px 3px rgba(0,0,0,0.2); 570 + } 571 + /* Toggle Switch */ 572 + .setting-toggle { 573 + display: flex; 574 + align-items: center; 575 + justify-content: space-between; 576 + } 577 + .toggle-switch { 578 + position: relative; 579 + width: 44px; 580 + height: 24px; 581 + flex-shrink: 0; 582 + } 583 + .toggle-switch input { 584 + opacity: 0; 585 + width: 0; 586 + height: 0; 587 + } 588 + .toggle-slider { 589 + position: absolute; 590 + cursor: pointer; 591 + inset: 0; 592 + background: var(--border-color); 593 + border-radius: 24px; 594 + transition: 0.2s; 595 + } 596 + .toggle-slider:before { 597 + position: absolute; 598 + content: ""; 599 + height: 18px; 600 + width: 18px; 601 + left: 3px; 602 + bottom: 3px; 603 + background: white; 604 + border-radius: 50%; 605 + transition: 0.2s; 606 + } 607 + .toggle-switch input:checked + .toggle-slider { 608 + background: var(--accent-color); 609 + } 610 + .toggle-switch input:checked + .toggle-slider:before { 611 + transform: translateX(20px); 612 + } 613 + .setting-desc { 614 + font-size: 0.75em; 615 + color: var(--label-color); 616 + margin-top: 4px; 617 + line-height: 1.3; 618 + } 619 + /* Rotation Buttons */ 620 + .rotation-buttons { 621 + display: flex; 622 + gap: 0; 623 + background: var(--border-color); 624 + border-radius: 6px; 625 + padding: 3px; 626 + } 627 + .rotation-btn { 628 + display: flex; 629 + align-items: center; 630 + gap: 4px; 631 + padding: 6px 14px; 632 + background: transparent; 633 + border: none; 634 + border-radius: 4px; 635 + font-size: 0.75em; 636 + font-weight: 500; 637 + color: var(--label-color); 638 + cursor: pointer; 639 + transition: all 0.15s; 640 + white-space: nowrap; 641 + } 642 + .rotation-btn:hover { 643 + background: var(--accent-color-10); 644 + color: var(--accent-color); 645 + } 646 + .rotation-btn.active { 647 + background: var(--accent-color); 648 + color: white; 649 + box-shadow: 0 1px 3px rgba(0,0,0,0.2); 650 + } 651 + .rotation-btn .rot-icon { 652 + font-size: 1.1em; 653 + } 654 + /* Overlap Selector */ 655 + .overlap-selector { 656 + display: flex; 657 + gap: 0; 658 + background: var(--border-color); 659 + border-radius: 6px; 660 + padding: 3px; 661 + } 662 + .overlap-btn { 663 + padding: 6px 14px; 664 + background: transparent; 665 + border: none; 666 + border-radius: 4px; 667 + font-size: 0.75em; 668 + font-weight: 500; 669 + color: var(--label-color); 670 + cursor: pointer; 671 + transition: all 0.15s; 672 + white-space: nowrap; 673 + } 674 + .overlap-btn:hover { 675 + background: var(--accent-color-10); 676 + color: var(--accent-color); 677 + } 678 + .overlap-btn.active { 679 + background: var(--accent-color); 680 + color: white; 681 + box-shadow: 0 1px 3px rgba(0,0,0,0.2); 682 + } 683 + /* Image Picker Grid Styles */ 684 + .image-picker-section { 685 + margin-top: 15px; 686 + padding: 12px; 687 + border: 1px solid var(--border-color); 688 + border-radius: 6px; 689 + max-height: 350px; 690 + overflow-y: auto; 691 + } 692 + .image-picker-header { 693 + display: flex; 694 + justify-content: space-between; 695 + align-items: center; 696 + margin-bottom: 10px; 697 + } 698 + .image-picker-title { 699 + font-size: 0.85em; 700 + font-weight: 600; 701 + color: var(--title-color); 702 + } 703 + .image-picker-count { 704 + font-size: 0.75em; 705 + color: var(--label-color); 706 + } 707 + .image-grid { 708 + display: grid; 709 + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 710 + gap: 8px; 711 + } 712 + .image-item { 713 + position: relative; 714 + border: 2px solid var(--border-color); 715 + border-radius: 6px; 716 + overflow: hidden; 717 + cursor: pointer; 718 + transition: all 0.2s; 719 + background: var(--card-bg); 720 + } 721 + .image-item:hover { 722 + border-color: var(--accent-color); 723 + transform: scale(1.02); 724 + } 725 + .image-item.state-0 { 726 + border-color: var(--border-color); 727 + } 728 + .image-item.state-1 { 729 + border-color: #3498db; 730 + background: rgba(52, 152, 219, 0.1); 731 + } 732 + .image-item.state-2 { 733 + border-color: #e74c3c; 734 + background: rgba(231, 76, 60, 0.1); 735 + } 736 + .image-item.state-3 { 737 + border-color: #9b59b6; 738 + background: rgba(155, 89, 182, 0.1); 739 + } 740 + .image-item.cover-locked { 741 + border-color: #e67e22; 742 + background: rgba(230, 126, 34, 0.1); 743 + cursor: default; 744 + opacity: 0.85; 745 + } 746 + .image-item.cover-locked:hover { 747 + transform: none; 748 + border-color: #e67e22; 749 + } 750 + .image-item.cover-locked .image-state-badge { 751 + background: #e67e22; 752 + } 753 + .image-item.separator-locked { 754 + border-color: #1abc9c; 755 + background: rgba(26, 188, 156, 0.1); 756 + cursor: default; 757 + opacity: 0.85; 758 + } 759 + .image-item.separator-locked:hover { 760 + transform: none; 761 + border-color: #1abc9c; 762 + } 763 + .image-item.separator-locked .image-state-badge { 764 + background: #1abc9c; 765 + } 766 + .image-item img { 767 + width: 100%; 768 + height: 80px; 769 + object-fit: contain; 770 + display: block; 771 + background: #fff; 772 + } 773 + .image-state-badge { 774 + position: absolute; 775 + top: 2px; 776 + right: 2px; 777 + padding: 2px 6px; 778 + border-radius: 4px; 779 + font-size: 0.65em; 780 + font-weight: 600; 781 + color: white; 782 + } 783 + .state-0 .image-state-badge { 784 + background: var(--label-color); 785 + } 786 + .state-1 .image-state-badge { 787 + background: #3498db; 788 + } 789 + .state-2 .image-state-badge { 790 + background: #e74c3c; 791 + } 792 + .state-3 .image-state-badge { 793 + background: #9b59b6; 794 + } 795 + /* Preview Overlay (shown on hover) */ 796 + .image-preview-overlay { 797 + position: absolute; 798 + top: 0; 799 + left: 0; 800 + right: 0; 801 + bottom: 20px; /* Above the filename */ 802 + pointer-events: none; 803 + opacity: 0; 804 + transition: opacity 0.15s; 805 + z-index: 5; 806 + display: flex; 807 + align-items: center; 808 + justify-content: center; 809 + } 810 + .image-item:hover .image-preview-overlay { 811 + opacity: 1; 812 + } 813 + /* Split lines - thick colored stripes */ 814 + .split-lines { 815 + position: absolute; 816 + top: 0; 817 + left: 0; 818 + right: 0; 819 + bottom: 0; 820 + } 821 + .split-line { 822 + position: absolute; 823 + top: 0; 824 + bottom: 0; 825 + width: 4px; 826 + transform: translateX(-50%); 827 + border-radius: 2px; 828 + } 829 + .split-line.split-h { 830 + background: linear-gradient(180deg, #3498db 0%, #2980b9 100%); 831 + box-shadow: 0 0 6px rgba(0, 0, 0, 0.5), 0 0 2px rgba(52, 152, 219, 0.8); 832 + } 833 + .split-line.split-v { 834 + background: linear-gradient(180deg, #e74c3c 0%, #c0392b 100%); 835 + box-shadow: 0 0 6px rgba(0, 0, 0, 0.5), 0 0 2px rgba(231, 76, 60, 0.8); 836 + } 837 + /* Rotate image on hover for rotation states */ 838 + .image-item.rotate-cw:hover img { 839 + transform: rotate(90deg) scale(0.75); 840 + transition: transform 0.2s ease; 841 + } 842 + .image-item.rotate-ccw:hover img { 843 + transform: rotate(-90deg) scale(0.75); 844 + transition: transform 0.2s ease; 845 + } 846 + .image-item img { 847 + transition: transform 0.2s ease; 848 + } 849 + .image-name { 850 + font-size: 0.65em; 851 + color: var(--label-color); 852 + padding: 4px; 853 + white-space: nowrap; 854 + overflow: hidden; 855 + text-overflow: ellipsis; 856 + text-align: center; 857 + } 858 + .state-legend { 859 + display: flex; 860 + gap: 8px; 861 + font-size: 0.7em; 862 + margin: 10px 0; 863 + flex-wrap: wrap; 864 + padding: 8px 12px; 865 + background: rgba(0,0,0,0.02); 866 + border-radius: 6px; 867 + align-items: center; 868 + } 869 + .legend-item { 870 + display: flex; 871 + align-items: center; 872 + gap: 5px; 873 + white-space: nowrap; 874 + } 875 + .legend-item.locked { 876 + opacity: 0.7; 877 + } 878 + .legend-btn { 879 + display: flex; 880 + align-items: center; 881 + gap: 5px; 882 + white-space: nowrap; 883 + padding: 4px 10px; 884 + border: 1px solid var(--btn-color); 885 + border-radius: 4px; 886 + background: transparent; 887 + color: var(--text-color); 888 + cursor: pointer; 889 + transition: all 0.15s; 890 + font-size: inherit; 891 + font-family: inherit; 892 + } 893 + .legend-btn:hover { 894 + background: var(--btn-color); 895 + color: white; 896 + } 897 + .legend-btn .legend-color { 898 + background: var(--btn-color); 899 + } 900 + .legend-color { 901 + width: 10px; 902 + height: 10px; 903 + border-radius: 3px; 904 + } 250 905 .file-info { 251 906 color: var(--label-color); 252 907 font-size: 0.85em; ··· 306 961 width: 0%; 307 962 transition: width 0.3s; 308 963 } 964 + #progress-fill.no-transition { 965 + transition: none !important; 966 + } 309 967 #progress-text { 310 968 text-align: center; 311 969 margin-top: 5px; ··· 336 994 } 337 995 .folder-btn:hover { 338 996 background-color: #d68910; 997 + } 998 + .folder-btn:disabled { 999 + background-color: #bdc3c7; 1000 + cursor: not-allowed; 339 1001 } 340 1002 /* Action button styles */ 341 1003 .delete-btn, ··· 636 1298 padding: 20px; 637 1299 font-size: 0.9em; 638 1300 } 1301 + .handedness-btn { 1302 + padding: 6px 10px; 1303 + } 1304 + .handedness-buttons { 1305 + width: 100%; 1306 + justify-content: center; 1307 + } 1308 + .quality-slider-container-inline { 1309 + max-width: 215px; 1310 + gap: 4px; 1311 + } 1312 + .quality-slider-container-inline .quality-slider { 1313 + min-width: 40px; 1314 + } 1315 + .quality-slider-container-inline .quality-input { 1316 + width: 28px; 1317 + } 1318 + } 1319 + /* Enhanced Logging System Styles */ 1320 + .log-section { 1321 + margin-top: 15px; 1322 + display: none; 1323 + } 1324 + .log-section.visible { 1325 + display: block; 1326 + } 1327 + .log-header { 1328 + display: flex; 1329 + justify-content: space-between; 1330 + align-items: center; 1331 + margin-bottom: 10px; 1332 + } 1333 + .log-title { 1334 + font-weight: 600; 1335 + color: var(--title-color); 1336 + font-size: 0.95em; 1337 + } 1338 + .log-container { 1339 + background: var(--card-bg); 1340 + border: 1px solid var(--border-color); 1341 + border-radius: 6px; 1342 + padding: 12px; 1343 + max-height: 300px; 1344 + overflow-y: auto; 1345 + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace; 1346 + font-size: 0.8em; 1347 + } 1348 + .log-entry { 1349 + color: var(--font-color); 1350 + display: flex; 1351 + gap: 8px; 1352 + align-items: flex-start; 1353 + margin-bottom: 4px; 1354 + line-height: 1.4; 1355 + } 1356 + .log-entry.success { 1357 + color: var(--accent-color); 1358 + } 1359 + .log-entry.error { 1360 + color: #e74c3c; 1361 + } 1362 + .log-entry.warning { 1363 + color: #f39c12; 1364 + } 1365 + .log-entry.info { 1366 + color: #3498db; 1367 + } 1368 + .log-timestamp { 1369 + color: var(--label-color); 1370 + font-size: 0.75em; 1371 + min-width: 52px; 1372 + opacity: 0.7; 1373 + } 1374 + .log-tag { 1375 + font-size: 0.7em; 1376 + font-weight: 600; 1377 + padding: 2px 6px; 1378 + border-radius: 3px; 1379 + text-transform: uppercase; 1380 + min-width: 56px; 1381 + text-align: center; 1382 + } 1383 + .log-tag.convert { 1384 + background: rgba(110, 154, 130, 0.15); 1385 + color: var(--accent-color); 1386 + } 1387 + .log-tag.split { 1388 + background: rgba(243, 156, 18, 0.15); 1389 + color: #f39c12; 1390 + } 1391 + .log-tag.fix { 1392 + background: rgba(39, 174, 96, 0.15); 1393 + color: #27ae60; 1394 + } 1395 + .log-tag.skip { 1396 + background: rgba(127, 140, 141, 0.15); 1397 + color: #7f8c8d; 1398 + } 1399 + .log-tag.info { 1400 + background: rgba(52, 152, 219, 0.15); 1401 + color: #3498db; 1402 + } 1403 + .log-tag.error { 1404 + background: rgba(231, 76, 60, 0.15); 1405 + color: #e74c3c; 1406 + } 1407 + .log-tag.done { 1408 + background: rgba(39, 174, 96, 0.15); 1409 + color: #27ae60; 1410 + } 1411 + .log-message { 1412 + flex: 1; 1413 + word-break: break-word; 1414 + } 1415 + .log-detail { 1416 + color: var(--label-color); 1417 + font-size: 0.9em; 1418 + } 1419 + .log-summary { 1420 + background: rgba(110, 154, 130, 0.08); 1421 + border: 1px solid var(--border-color); 1422 + border-radius: 6px; 1423 + margin-top: 12px; 1424 + overflow: hidden; 1425 + } 1426 + .log-summary-title { 1427 + background: rgba(110, 154, 130, 0.15); 1428 + padding: 8px 12px; 1429 + font-weight: 600; 1430 + font-size: 0.85em; 1431 + border-bottom: 1px solid var(--border-color); 1432 + color: var(--accent-color); 1433 + } 1434 + .log-summary-table { 1435 + width: 100%; 1436 + font-size: 0.8em; 1437 + } 1438 + .log-summary-table td { 1439 + padding: 6px 12px; 1440 + border-bottom: 1px solid var(--border-color); 1441 + } 1442 + .log-summary-table tr:last-child td { 1443 + border-bottom: none; 1444 + } 1445 + .log-summary-table td:first-child { 1446 + color: var(--label-color); 1447 + width: 45%; 1448 + } 1449 + .log-summary-table td:last-child { 1450 + color: var(--font-color); 1451 + font-weight: 500; 1452 + text-align: right; 1453 + } 1454 + .log-summary-table .highlight { 1455 + color: var(--accent-color); 1456 + } 1457 + .log-summary-table .saved { 1458 + color: #27ae60; 1459 + } 1460 + .log-summary-table .increased { 1461 + color: #f39c12; 639 1462 } 640 1463 </style> 641 1464 </head> ··· 655 1478 <div class="action-buttons"> 656 1479 <button class="action-btn upload-action-btn" onclick="openUploadModal()">📤 Upload</button> 657 1480 <button class="action-btn folder-action-btn" onclick="openFolderModal()">📁 New Folder</button> 658 - <button class="action-btn" style="background-color:#e74c3c" onclick="openDeleteSelectedModal()">🗑️ Delete Selected</button> 1481 + <button class="action-btn delete-action-btn" onclick="openDeleteSelectedModal()">🗑️ Delete Selected</button> 659 1482 </div> 660 1483 </div> 661 1484 ··· 691 1514 <!-- Upload Modal --> 692 1515 <div class="modal-overlay" id="uploadModal"> 693 1516 <div class="modal"> 694 - <button class="modal-close" onclick="closeUploadModal()">&times;</button> 1517 + <button class="modal-close" id="uploadModalClose" onclick="closeUploadModal()">&times;</button> 695 1518 <h3>📤 Upload file</h3> 696 1519 <div class="upload-form"> 697 1520 <p class="file-info">Select a file to upload to <strong id="uploadPathDisplay"></strong></p> 698 1521 <input type="file" id="fileInput" onchange="validateFile()" multiple> 1522 + <div class="picker-columns" id="pickerColumns"> 1523 + <div class="convert-options" id="convertOptions" style="display:none;"> 1524 + <div class="convert-row"> 1525 + <label class="convert-checkbox"> 1526 + <input type="checkbox" id="convertBeforeUpload" onchange="toggleConvertOptions()"> 1527 + <span>Optimize EPUB</span> 1528 + </label> 1529 + <span class="convert-separator">—</span> 1530 + <span class="advanced-options-toggle" id="advancedOptionsToggle" onclick="toggleAdvancedOptions()"> 1531 + <span class="advanced-options-arrow" id="advancedOptionsArrow">▶</span> 1532 + <span class="advanced-options-text">Advanced Mode</span> 1533 + </span> 1534 + </div> 1535 + 1536 + <div id="convertInfo" style="display:none; margin-bottom: 10px; font-size: 0.9em;"> 1537 + </div> 1538 + 1539 + <div class="convert-settings" id="convertSettings" style="display:block;"> 1540 + <span>⚫ True-Grayscale</span> 1541 + <span>📏 Max 480×800px</span> 1542 + <span>📦 85% JPEG</span> 1543 + <span>🔧 Fix SVG</span> 1544 + </div> 1545 + <!-- Advanced Settings Content --> 1546 + <div class="advanced-settings-content" id="advancedSettingsContent"> 1547 + <!-- JPEG Quality --> 1548 + <div class="advanced-setting-row quality-row"> 1549 + <div class="setting-title">📦 JPEG Quality</div> 1550 + <div class="quality-presets"> 1551 + <button class="quality-preset" data-value="30" onclick="setQualityPreset(30)" title="Minimum quality">30%</button> 1552 + <button class="quality-preset" data-value="45" onclick="setQualityPreset(45)" title="Low quality">45%</button> 1553 + <button class="quality-preset" data-value="60" onclick="setQualityPreset(60)" title="Medium-low quality">60%</button> 1554 + <button class="quality-preset" data-value="75" onclick="setQualityPreset(75)" title="Medium quality">75%</button> 1555 + <button class="quality-preset active" data-value="85" onclick="setQualityPreset(85)" title="High quality">85%</button> 1556 + <button class="quality-preset" data-value="95" onclick="setQualityPreset(95)" title="Maximum quality">95%</button> 1557 + </div> 1558 + <!-- Hidden inputs to maintain compatibility --> 1559 + <input type="hidden" id="qualitySlider" value="85"> 1560 + <input type="hidden" id="qualityInput" value="85"> 1561 + </div> 1562 + <!-- Grayscale Toggle (commented for e-ink optimization, kept for compatibility with other devices/web apps) 1563 + <div class="advanced-setting-row" style="display:none;"> 1564 + <div class="setting-toggle"> 1565 + <div> 1566 + <div class="setting-title">⚫ True-Grayscale</div> 1567 + <div class="setting-desc">BT.601 luminance formula (recommended for e-ink)</div> 1568 + </div> 1569 + <label class="toggle-switch"> 1570 + <input type="checkbox" id="grayscaleToggle" checked onchange="updateQualitySettings()"> 1571 + <span class="toggle-slider"></span> 1572 + </label> 1573 + </div> 1574 + </div> 1575 + --> 1576 + <!-- Rotation Direction --> 1577 + <div class="advanced-setting-row" id="rotationSettingRow"> 1578 + <div class="setting-title">↻ Rotation Direction</div> 1579 + <div class="setting-controls"> 1580 + <div class="rotation-buttons"> 1581 + <button class="rotation-btn" id="rotationCCW" onclick="setHandedness('left')"> 1582 + <span class="rot-icon">↺</span> CCW 1583 + </button> 1584 + <button class="rotation-btn active" id="rotationCW" onclick="setHandedness('right')"> 1585 + <span class="rot-icon">↻</span> CW 1586 + </button> 1587 + </div> 1588 + </div> 1589 + </div> 1590 + <!-- Overlap Settings --> 1591 + <div class="advanced-setting-row" id="overlapSettingRow"> 1592 + <div class="setting-title">📐 Min Overlap</div> 1593 + <div class="setting-controls"> 1594 + <div class="overlap-selector"> 1595 + <button class="overlap-btn active" data-value="5" onclick="setOverlap(5)">5%</button> 1596 + <button class="overlap-btn" data-value="10" onclick="setOverlap(10)">10%</button> 1597 + <button class="overlap-btn" data-value="15" onclick="setOverlap(15)">15%</button> 1598 + </div> 1599 + </div> 1600 + </div> 1601 + <!-- Conversion Log Export --> 1602 + <div class="advanced-setting-row"> 1603 + <div class="setting-label"> 1604 + <div class="setting-title">📥 Auto-download Log</div> 1605 + <div class="setting-desc">Export detailed log with statistics</div> 1606 + </div> 1607 + <div class="setting-controls"> 1608 + <label class="toggle-switch"> 1609 + <input type="checkbox" id="export-log-checkbox"> 1610 + <span class="toggle-slider"></span> 1611 + </label> 1612 + </div> 1613 + </div> 1614 + </div><!-- advanced-settings-content --> 1615 + </div><!-- end convert-options --> 1616 + <!-- Image Picker Section - in picker-columns for two-column layout --> 1617 + <div class="image-picker-section" id="imagePickerSection" style="display:none;"> 1618 + <div class="image-picker-header"> 1619 + <span class="image-picker-title">🖼️ Image Processing Options</span> 1620 + <span class="image-picker-count" id="imagePickerCount">0 images</span> 1621 + </div> 1622 + <div class="state-legend"> 1623 + <div class="legend-item locked"> 1624 + <span>🔒</span> 1625 + <span>Cover</span> 1626 + </div> 1627 + <div class="legend-item locked"> 1628 + <span style="color:#1abc9c">✦</span> 1629 + <span>Separator</span> 1630 + </div> 1631 + <button class="legend-btn" style="--btn-color:var(--label-color)" onclick="applyStateToAll(0)"> 1632 + <div class="legend-color"></div> 1633 + <span>Normal</span> 1634 + </button> 1635 + <button class="legend-btn" style="--btn-color:#3498db" onclick="applyStateToAll(1)"> 1636 + <div class="legend-color"></div> 1637 + <span>H-Split</span> 1638 + </button> 1639 + <button class="legend-btn" style="--btn-color:#e74c3c" onclick="applyStateToAll(2)"> 1640 + <div class="legend-color"></div> 1641 + <span>V-Split</span> 1642 + </button> 1643 + <button class="legend-btn" style="--btn-color:#9b59b6" onclick="applyStateToAll(3)"> 1644 + <div class="legend-color"></div> 1645 + <span>Rotate</span> 1646 + </button> 1647 + </div> 1648 + <div class="image-grid" id="imageGrid"></div> 1649 + </div> 1650 + </div><!-- end picker-columns --> 1651 + <!-- Sync compatibility warning - outside picker-columns --> 1652 + <div class="convert-warning" id="convertWarning" style="display:none;" role="alert" aria-live="polite"> 1653 + ⚠️ Converting will modify files and can break hash‑based sync. ⚠️<br> 1654 + Please back up or disable sync before proceeding. 1655 + </div> 1656 + <!-- Optimize & Upload button - shown when picker is visible, replaces Upload --> 1657 + <button id="startConversionBtn" class="upload-btn optimize" style="display:none;" onclick="startConversionWithImageStates()">Optimize & Upload</button> 699 1658 <button id="uploadBtn" class="upload-btn" onclick="uploadFile()" disabled>Upload</button> 1659 + <button class="delete-btn-cancel" onclick="handleCancelUploadModal()">Cancel</button> 700 1660 <div id="progress-container"> 701 1661 <div id="progress-bar"><div id="progress-fill"></div></div> 702 1662 <div id="progress-text"></div> 703 1663 </div> 1664 + <div class="log-section" id="log-section"> 1665 + <div class="log-header"> 1666 + <div class="log-title">📋 Conversion Log</div> 1667 + </div> 1668 + <div class="log-container" id="log-container"></div> 1669 + </div> 704 1670 </div> 705 1671 </div> 706 1672 </div> ··· 712 1678 <h3>📁 New Folder</h3> 713 1679 <div class="folder-form"> 714 1680 <p class="file-info">Create a new folder in <strong id="folderPathDisplay"></strong></p> 715 - <input type="text" id="folderName" class="folder-input" placeholder="Folder name..."> 716 - <button class="folder-btn" onclick="createFolder()">Create Folder</button> 1681 + <input type="text" id="folderName" class="folder-input" placeholder="Folder name..." oninput="document.getElementById('createFolderBtn').disabled = !this.value.trim();"> 1682 + <button class="folder-btn" id="createFolderBtn" onclick="createFolder()" disabled>Create Folder</button> 1683 + <button class="delete-btn-cancel" onclick="closeFolderModal()">Cancel</button> 717 1684 </div> 718 1685 </div> 719 1686 </div> ··· 767 1734 <script> 768 1735 // get current path from query parameter 769 1736 const currentPath = decodeURIComponent(new URLSearchParams(window.location.search).get('path') || '/'); 1737 + 1738 + // Network status monitoring 1739 + let isNetworkOnline = navigator.onLine; 1740 + 1741 + // Add network status listeners 1742 + window.addEventListener('online', () => { 1743 + console.log('[Network] Online event fired'); 1744 + isNetworkOnline = true; 1745 + showNotification('Network connection restored', 'success'); 1746 + }); 1747 + 1748 + window.addEventListener('offline', () => { 1749 + console.log('[Network] Offline event fired'); 1750 + isNetworkOnline = false; 1751 + showNotification('Network connection lost', 'warning'); 1752 + }); 1753 + 1754 + // Initialize network status 1755 + console.log('[Network] Initial status:', isNetworkOnline ? 'online' : 'offline'); 770 1756 771 1757 function escapeHtml(unsafe) { 772 1758 return unsafe ··· 777 1763 .replaceAll("'", "&#039;"); 778 1764 } 779 1765 1766 + function showNotification(message, type = 'info') { 1767 + // Create notification element if it doesn't exist 1768 + let notification = document.getElementById('notification'); 1769 + if (!notification) { 1770 + notification = document.createElement('div'); 1771 + notification.id = 'notification'; 1772 + notification.style.cssText = ` 1773 + position: fixed; 1774 + top: 20px; 1775 + right: 20px; 1776 + padding: 12px 20px; 1777 + border-radius: 4px; 1778 + color: white; 1779 + font-weight: 500; 1780 + z-index: 10000; 1781 + max-width: 300px; 1782 + box-shadow: 0 2px 8px rgba(0,0,0,0.2); 1783 + transition: all 0.3s ease; 1784 + `; 1785 + document.body.appendChild(notification); 1786 + } 1787 + 1788 + // Set styles based on type 1789 + const styles = { 1790 + 'success': 'background-color: #27ae60;', 1791 + 'error': 'background-color: #e74c3c;', 1792 + 'warning': 'background-color: #f39c12;', 1793 + 'info': 'background-color: #3498db;' 1794 + }; 1795 + notification.style.cssText += styles[type] || styles['info']; 1796 + notification.textContent = message; 1797 + 1798 + // Show notification 1799 + notification.style.opacity = '1'; 1800 + notification.style.transform = 'translateX(0)'; 1801 + 1802 + // Auto-hide after 5 seconds 1803 + setTimeout(() => { 1804 + notification.style.opacity = '0'; 1805 + notification.style.transform = 'translateX(100%)'; 1806 + setTimeout(() => { 1807 + if (notification.parentNode) { 1808 + notification.parentNode.removeChild(notification); 1809 + } 1810 + }, 300); 1811 + }, 5000); 1812 + } 1813 + 780 1814 function formatFileSize(bytes) { 781 1815 if (bytes === 0) return '0 B'; 782 1816 const k = 1024; ··· 786 1820 } 787 1821 788 1822 async function hydrate() { 789 - // Close modals when clicking overlay 1823 + // Fetch CrossPoint version 1824 + fetchVersion(); 1825 + 1826 + // Close modals when clicking overlay - call proper cleanup functions 790 1827 document.querySelectorAll('.modal-overlay').forEach(function(overlay) { 791 1828 overlay.addEventListener('click', function(e) { 792 1829 if (e.target === overlay) { 1830 + // Call the appropriate close function for each modal to ensure cleanup 1831 + if (overlay.id === 'uploadModal') return closeUploadModal(); 1832 + if (overlay.id === 'folderModal') return closeFolderModal(); 1833 + if (overlay.id === 'deleteModal') return closeDeleteModal(); 1834 + if (overlay.id === 'renameModal') return closeRenameModal(); 1835 + if (overlay.id === 'moveModal') return closeMoveModal(); 793 1836 overlay.classList.remove('open'); 794 1837 } 795 1838 }); ··· 814 1857 815 1858 let files = []; 816 1859 try { 817 - const response = await fetch('/api/files?path=' + encodeURIComponent(currentPath)); 1860 + const response = await fetch('/api/files?path=' + encodeURIComponent(currentPath) + '&_=' + Date.now()); 818 1861 if (!response.ok) { 819 1862 throw new Error('Failed to load files: ' + response.status + ' ' + response.statusText); 820 1863 } ··· 896 1939 897 1940 // Modal functions 898 1941 function openUploadModal() { 1942 + // Reset converter variables to defaults 1943 + ENABLE_GRAYSCALE = true; 1944 + JPEG_QUALITY = 85; 1945 + HANDEDNESS = 'right'; 1946 + OVERLAP_PERCENT = 5; 1947 + imageStates = {}; 1948 + 1949 + // Hide convert options when opening modal (no files selected initially) 1950 + const convertOptions = document.getElementById('convertOptions'); 1951 + if (convertOptions) { 1952 + convertOptions.style.display = 'none'; 1953 + } 1954 + 1955 + // Reset rotation and overlap UI 1956 + setHandedness('right'); 1957 + setOverlap(5); 1958 + 1959 + // Hide log section from previous session 1960 + const logSection = document.getElementById('log-section'); 1961 + if (logSection) logSection.classList.remove('visible'); 1962 + const logContainer = document.getElementById('log-container'); 1963 + if (logContainer) logContainer.innerHTML = ''; 1964 + 899 1965 document.getElementById('uploadPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath; 900 1966 document.getElementById('uploadModal').classList.add('open'); 901 1967 } 902 1968 1969 + function handleCancelUploadModal() { 1970 + if (!isUploadInProgress) { 1971 + // No process running: close the modal normally 1972 + closeUploadModal(); 1973 + return; 1974 + } 1975 + // Process running: stop it, keep modal open for retry 1976 + operationCancelled = true; 1977 + if (currentUploadWs) { currentUploadWs.close(); currentUploadWs = null; } 1978 + if (currentUploadXhr) { currentUploadXhr.abort(); currentUploadXhr = null; } 1979 + // isUploadInProgress and UI are restored by restoreAfterCancel() from the async handlers 1980 + } 1981 + 1982 + function restoreAfterCancel() { 1983 + operationCancelled = false; 1984 + isUploadInProgress = false; 1985 + document.getElementById('uploadModalClose').classList.remove('disabled'); 1986 + const progressFill = document.getElementById('progress-fill'); 1987 + const progressText = document.getElementById('progress-text'); 1988 + progressFill.style.width = '0%'; 1989 + progressFill.style.backgroundColor = '#e74c3c'; 1990 + progressText.style.color = '#e74c3c'; 1991 + progressText.textContent = 'Upload cancelled by User!'; 1992 + // Re-enable the action button (uploadBtn is always visible at this point; 1993 + // its text is already "Upload" or "Optimize & Upload" depending on checkbox state) 1994 + document.getElementById('uploadBtn').disabled = false; 1995 + } 1996 + 903 1997 function closeUploadModal() { 1998 + // Prevent closing during upload/conversion 1999 + if (isUploadInProgress) { 2000 + return; 2001 + } 904 2002 document.getElementById('uploadModal').classList.remove('open'); 905 - document.getElementById('fileInput').value = ''; 906 - document.getElementById('uploadBtn').disabled = true; 2003 + const fileInput = document.getElementById('fileInput'); 2004 + fileInput.value = ''; 2005 + fileInput.classList.remove('has-files'); 2006 + const uploadBtn = document.getElementById('uploadBtn'); 2007 + uploadBtn.disabled = true; 2008 + uploadBtn.style.display = 'block'; 2009 + uploadBtn.textContent = 'Upload'; 2010 + uploadBtn.classList.remove('optimize'); 2011 + document.getElementById('startConversionBtn').style.display = 'none'; 907 2012 document.getElementById('progress-container').style.display = 'none'; 908 2013 document.getElementById('progress-fill').style.width = '0%'; 909 2014 document.getElementById('progress-fill').style.backgroundColor = '#27ae60'; 2015 + document.getElementById('convertBeforeUpload').checked = false; 2016 + document.getElementById('convertInfo').style.display = 'none'; 2017 + document.getElementById('convertWarning').style.display = 'none'; 2018 + // Clear image picker cache and reset layout 2019 + epubImagesCache = []; 2020 + imageStates = {}; 2021 + document.getElementById('imagePickerSection').style.display = 'none'; 2022 + const imageGrid = document.getElementById('imageGrid'); 2023 + if (imageGrid) imageGrid.innerHTML = ''; 2024 + document.querySelector('#uploadModal .modal').classList.remove('picker-mode'); 2025 + document.getElementById('pickerColumns').classList.remove('picker-active'); 2026 + // Hide log section 2027 + if (logSection) logSection.classList.remove('visible'); 2028 + // Reset advanced options 2029 + const advancedSettingsContent = document.getElementById('advancedSettingsContent'); 2030 + if (advancedSettingsContent) advancedSettingsContent.classList.remove('visible'); 2031 + document.getElementById('advancedOptionsArrow').classList.remove('expanded'); 2032 + const advancedOptionsToggle = document.getElementById('advancedOptionsToggle'); 2033 + if (advancedOptionsToggle) { 2034 + advancedOptionsToggle.style.opacity = '0.5'; 2035 + advancedOptionsToggle.style.pointerEvents = 'none'; 2036 + } 2037 + // Reset to defaults 2038 + document.getElementById('qualitySlider').value = 85; 2039 + document.getElementById('qualityInput').value = 85; 2040 + setHandedness('right'); 2041 + setOverlap(5); 2042 + // Update converter variables 2043 + updateQualitySettings(); 910 2044 } 911 2045 2046 + function updateBatchModeUI(isBatch) { 2047 + const rotationRow = document.getElementById('rotationSettingRow'); 2048 + const overlapRow = document.getElementById('overlapSettingRow'); 2049 + if (rotationRow) rotationRow.style.display = isBatch ? 'none' : ''; 2050 + if (overlapRow) overlapRow.style.display = isBatch ? 'none' : ''; 2051 + } 2052 + 2053 + function toggleConvertOptions() { 2054 + const checked = document.getElementById('convertBeforeUpload').checked; 2055 + const uploadBtn = document.getElementById('uploadBtn'); 2056 + document.getElementById('convertWarning').style.display = checked ? 'block' : 'none'; 2057 + document.getElementById('convertInfo').style.display = checked ? 'block' : 'none'; 2058 + // Update button text and style 2059 + if (checked) { 2060 + uploadBtn.textContent = 'Optimize & Upload'; 2061 + uploadBtn.classList.add('optimize'); 2062 + } else { 2063 + uploadBtn.textContent = 'Upload'; 2064 + uploadBtn.classList.remove('optimize'); 2065 + // Clear image picker when unchecking 2066 + clearImagePicker(); 2067 + } 2068 + // Reset advanced options when toggling off 2069 + if (!checked) { 2070 + const advancedSettingsContent = document.getElementById('advancedSettingsContent'); 2071 + if (advancedSettingsContent) advancedSettingsContent.classList.remove('visible'); 2072 + document.getElementById('advancedOptionsArrow').classList.remove('expanded'); 2073 + } 2074 + // Disable/enable advanced mode toggle 2075 + const advancedOptionsToggle = document.getElementById('advancedOptionsToggle'); 2076 + if (advancedOptionsToggle) { 2077 + advancedOptionsToggle.style.opacity = checked ? '1' : '0.5'; 2078 + advancedOptionsToggle.style.pointerEvents = checked ? 'auto' : 'none'; 2079 + } 2080 + } 2081 + 2082 + function toggleAdvancedOptions() { 2083 + // Check if advanced options toggle is enabled (optimize EPUB must be checked) 2084 + const convertEnabled = document.getElementById('convertBeforeUpload').checked; 2085 + if (!convertEnabled) { 2086 + return; // Don't toggle if optimization is disabled 2087 + } 2088 + 2089 + const content = document.getElementById('advancedSettingsContent'); 2090 + const arrow = document.getElementById('advancedOptionsArrow'); 2091 + const isExpanding = !content.classList.contains('visible'); 2092 + 2093 + content.classList.toggle('visible'); 2094 + arrow.classList.toggle('expanded'); 2095 + 2096 + // When expanding, show image picker if an EPUB is selected 2097 + if (isExpanding) { 2098 + const fileInput = document.getElementById('fileInput'); 2099 + const files = fileInput.files; 2100 + if (files.length === 1 && files[0].name.toLowerCase().endsWith('.epub')) { 2101 + showImagePicker(files[0]).catch(err => console.error('Image picker error:', err)); 2102 + } 2103 + } else { 2104 + // When collapsing, hide the picker and startConversionBtn 2105 + document.getElementById('imagePickerSection').style.display = 'none'; 2106 + document.getElementById('startConversionBtn').style.display = 'none'; 2107 + document.getElementById('uploadBtn').style.display = 'block'; 2108 + document.getElementById('uploadBtn').disabled = false; 2109 + document.querySelector('#uploadModal .modal').classList.remove('picker-mode'); 2110 + document.getElementById('pickerColumns').classList.remove('picker-active'); 2111 + } 2112 + } 2113 + 2114 + function setQualityPreset(value) { 2115 + document.getElementById('qualitySlider').value = value; 2116 + document.getElementById('qualityInput').value = value; 2117 + // Update active preset 2118 + document.querySelectorAll('.quality-preset').forEach(btn => { 2119 + btn.classList.remove('active'); 2120 + if (parseInt(btn.dataset.value, 10) === value) { 2121 + btn.classList.add('active'); 2122 + } 2123 + }); 2124 + updateQualitySettings(); 2125 + } 2126 + 2127 + function updateQualitySettings() { 2128 + const quality = document.getElementById('qualitySlider').value; 2129 + // Check if grayscaleToggle exists (may be hidden for compatibility with other devices) 2130 + const grayscaleToggle = document.getElementById('grayscaleToggle'); 2131 + const grayscale = grayscaleToggle ? grayscaleToggle.checked : true; // Default to true for e-ink 2132 + 2133 + // Update displays (if element exists) 2134 + const qualityDisplay = document.getElementById('qualityDisplaySimple'); 2135 + if (qualityDisplay) { 2136 + qualityDisplay.textContent = '📦 ' + quality + '% JPEG'; 2137 + } 2138 + 2139 + // Update converter variables (used by processImage and applyGrayscale) 2140 + JPEG_QUALITY = parseInt(quality, 10); 2141 + ENABLE_GRAYSCALE = true; // Always grayscale for e-ink 2142 + } 2143 + 2144 + function setHandedness(value) { 2145 + HANDEDNESS = value; 2146 + // Update UI 2147 + document.getElementById('rotationCW').classList.remove('active'); 2148 + document.getElementById('rotationCCW').classList.remove('active'); 2149 + document.getElementById(value === 'right' ? 'rotationCW' : 'rotationCCW').classList.add('active'); 2150 + // Re-render grid to update rotation arrows 2151 + if (document.getElementById('imagePickerSection').style.display !== 'none') { 2152 + renderImageGrid(); 2153 + } 2154 + } 2155 + 2156 + function setOverlap(value) { 2157 + OVERLAP_PERCENT = value; 2158 + // Update UI 2159 + document.querySelectorAll('.overlap-btn').forEach(btn => { 2160 + btn.classList.toggle('active', parseInt(btn.dataset.value) === value); 2161 + }); 2162 + } 2163 + 2164 + // ============================================================================ 2165 + // Image Picker Functions 2166 + // ============================================================================ 2167 + 2168 + /** 2169 + * Extract images from EPUB for preview 2170 + * Returns array of {path, name, dataUrl, width, height} 2171 + */ 2172 + async function extractImagesForPreview(file) { 2173 + const zip = await JSZip.loadAsync(file); 2174 + const imageExtensions = ['.png', '.gif', '.webp', '.bmp', '.jpg', '.jpeg']; 2175 + 2176 + // Collect all image paths first 2177 + const allImages = []; 2178 + for (const [path, fileObj] of Object.entries(zip.files)) { 2179 + if (fileObj.dir) continue; 2180 + const ext = path.substring(path.lastIndexOf('.')).toLowerCase(); 2181 + if (imageExtensions.includes(ext)) { 2182 + allImages.push(path); 2183 + } 2184 + } 2185 + 2186 + // Try to get images in reading order from OPF spine 2187 + let orderedImages = []; 2188 + let coverImagePath = null; // Track cover image 2189 + try { 2190 + // Find OPF file 2191 + let opfPath = null; 2192 + zip.forEach(p => { if (p.toLowerCase().endsWith('.opf')) opfPath = p; }); 2193 + 2194 + if (opfPath) { 2195 + const opfContent = await zip.files[opfPath].async('string'); 2196 + const opfDir = opfPath.includes('/') ? opfPath.substring(0, opfPath.lastIndexOf('/')) : ''; 2197 + 2198 + // Detect cover image from OPF 2199 + let coverId = null; 2200 + let m; 2201 + // Try 1: properties="cover-image" 2202 + if (m = opfContent.match(/<item[^>]+id=["']([^"']+)["'][^>]+properties="[^"]*cover-image[^"]*"/)) coverId = m[1]; 2203 + if (!coverId && (m = opfContent.match(/<item[^>]+properties="[^"]*cover-image[^"]*"[^>]+id=["']([^"']+)["']/))) coverId = m[1]; 2204 + // Try 2: meta name="cover" content="id" 2205 + if (!coverId && (m = opfContent.match(/<meta\s+name=["']cover["']\s+content=["']([^"']+)["']/))) coverId = m[1]; 2206 + if (!coverId && (m = opfContent.match(/<meta\s+content=["']([^"']+)["']\s+name=["']cover["']/))) coverId = m[1]; 2207 + 2208 + // Parse manifest to get id -> href mapping 2209 + const manifest = {}; 2210 + const manifestRegex = /<item[^>]+id=["']([^"']+)["'][^>]+href=["']([^"']+)["'][^>]*>/gi; 2211 + let match; 2212 + while ((match = manifestRegex.exec(opfContent)) !== null) { 2213 + const id = match[1]; 2214 + const href = match[2]; 2215 + const fullPath = opfDir ? opfDir + '/' + href : href; 2216 + manifest[id] = fullPath; 2217 + if (id === coverId) coverImagePath = fullPath; 2218 + } 2219 + // Also check reversed attribute order 2220 + const manifestRegex2 = /<item[^>]+href=["']([^"']+)["'][^>]+id=["']([^"']+)["'][^>]*>/gi; 2221 + while ((match = manifestRegex2.exec(opfContent)) !== null) { 2222 + const href = match[1]; 2223 + const id = match[2]; 2224 + const fullPath = opfDir ? opfDir + '/' + href : href; 2225 + manifest[id] = fullPath; 2226 + if (id === coverId) coverImagePath = fullPath; 2227 + } 2228 + 2229 + // Cover-page reconciliation — if the cover XHTML references a different 2230 + // but byte-identical image, prefer the one actually displayed on the page. 2231 + if (coverImagePath) { 2232 + try { 2233 + let coverXhtmlPath = null; 2234 + const guideM = opfContent.match(/<(?:\w+:)?reference[^>]+type=["']cover["'][^>]+href=["']([^"']+)["']/i) || 2235 + opfContent.match(/<(?:\w+:)?reference[^>]+href=["']([^"']+)["'][^>]+type=["']cover["']/i); 2236 + if (guideM) { 2237 + coverXhtmlPath = opfDir ? opfDir + '/' + decodeHref(guideM[1]) : decodeHref(guideM[1]); 2238 + } 2239 + if (!coverXhtmlPath) { 2240 + const spineM = opfContent.match(/<(?:\w+:)?itemref[^>]+idref=["']([^"']+)["']/i); 2241 + if (spineM && manifest[spineM[1]]) coverXhtmlPath = manifest[spineM[1]]; 2242 + } 2243 + if (coverXhtmlPath && zip.files[coverXhtmlPath]) { 2244 + const coverXhtml = await zip.files[coverXhtmlPath].async('string'); 2245 + const imgM = coverXhtml.match(/(?:src|xlink:href)=["']([^"']+)["']/i); 2246 + if (imgM) { 2247 + const href = imgM[1]; 2248 + const xDir = coverXhtmlPath.includes('/') ? coverXhtmlPath.substring(0, coverXhtmlPath.lastIndexOf('/')) : ''; 2249 + let pageImgPath = href.startsWith('../') ? xDir.split('/').slice(0, -1).join('/') + '/' + href.substring(3) 2250 + : href.startsWith('/') ? href.substring(1) 2251 + : xDir ? xDir + '/' + href : href; 2252 + pageImgPath = pageImgPath.replace(/\/+/g, '/'); 2253 + for (const realPath of allImages) { 2254 + if (realPath === pageImgPath || realPath.endsWith('/' + href) || realPath.endsWith(href)) { 2255 + pageImgPath = realPath; break; 2256 + } 2257 + } 2258 + if (pageImgPath !== coverImagePath && allImages.includes(pageImgPath) && zip.files[pageImgPath]) { 2259 + const coverData = await zip.files[coverImagePath].async('arraybuffer'); 2260 + const pageData = await zip.files[pageImgPath].async('arraybuffer'); 2261 + if (coverData.byteLength === pageData.byteLength) { 2262 + const a = new Uint8Array(coverData); 2263 + const b = new Uint8Array(pageData); 2264 + let identical = true; 2265 + for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { identical = false; break; } } 2266 + if (identical) coverImagePath = pageImgPath; 2267 + } 2268 + } 2269 + } 2270 + } 2271 + } catch (e) { /* non-critical */ } 2272 + } 2273 + 2274 + // Parse spine to get reading order 2275 + const spineOrder = []; 2276 + const spineRegex = /<itemref[^>]+idref=["']([^"']+)["'][^>]*>/gi; 2277 + while ((match = spineRegex.exec(opfContent)) !== null) { 2278 + const idref = match[1]; 2279 + if (manifest[idref]) spineOrder.push(manifest[idref]); 2280 + } 2281 + 2282 + // For each spine item (XHTML), extract images in order 2283 + const seenImages = new Set(); 2284 + for (const xhtmlPath of spineOrder) { 2285 + if (!zip.files[xhtmlPath]) continue; 2286 + const xhtmlContent = await zip.files[xhtmlPath].async('string'); 2287 + const xhtmlDir = xhtmlPath.includes('/') ? xhtmlPath.substring(0, xhtmlPath.lastIndexOf('/')) : ''; 2288 + 2289 + // Find all image references 2290 + const imgRegex = /(?:src|xlink:href)=["']([^"']+)["']/gi; 2291 + while ((match = imgRegex.exec(xhtmlContent)) !== null) { 2292 + let imgHref = match[1]; 2293 + // Skip non-image references 2294 + if (!imageExtensions.some(ext => imgHref.toLowerCase().endsWith(ext))) continue; 2295 + 2296 + // Resolve relative path 2297 + let imgPath; 2298 + if (imgHref.startsWith('../')) { 2299 + // Go up from xhtmlDir 2300 + const parts = xhtmlDir.split('/'); 2301 + parts.pop(); 2302 + imgPath = parts.join('/') + '/' + imgHref.substring(3); 2303 + } else if (imgHref.startsWith('/')) { 2304 + imgPath = imgHref.substring(1); 2305 + } else { 2306 + imgPath = xhtmlDir ? xhtmlDir + '/' + imgHref : imgHref; 2307 + } 2308 + // Normalize path 2309 + imgPath = imgPath.replace(/\/+/g, '/'); 2310 + 2311 + // Check if this is actually an image in our list 2312 + for (const realPath of allImages) { 2313 + if (realPath === imgPath || realPath.endsWith('/' + imgHref) || realPath.endsWith(imgHref)) { 2314 + if (!seenImages.has(realPath)) { 2315 + seenImages.add(realPath); 2316 + orderedImages.push(realPath); 2317 + } 2318 + break; 2319 + } 2320 + } 2321 + } 2322 + } 2323 + 2324 + // Add any remaining images that weren't in XHTML files (e.g., unused images) 2325 + for (const imgPath of allImages) { 2326 + if (!seenImages.has(imgPath)) { 2327 + orderedImages.push(imgPath); 2328 + } 2329 + } 2330 + } 2331 + } catch (e) { 2332 + console.warn('Failed to parse reading order, using default:', e); 2333 + } 2334 + 2335 + // Fallback to alphabetical if parsing failed 2336 + if (orderedImages.length === 0) { 2337 + orderedImages = [...allImages].sort(); 2338 + } 2339 + 2340 + // Load image data in order 2341 + const images = []; 2342 + for (const path of orderedImages) { 2343 + const data = await zip.files[path].async('arraybuffer'); 2344 + const blob = new Blob([data]); 2345 + const dataUrl = URL.createObjectURL(blob); 2346 + 2347 + // Get dimensions 2348 + const dims = await getImageDimensions(data); 2349 + 2350 + // Check if this is the cover image 2351 + const isCover = (path === coverImagePath) || 2352 + path.toLowerCase().includes('cover') && images.length === 0; 2353 + 2354 + // Check if this is a separator/ornament (skip cover check) 2355 + const filename = path.split('/').pop(); 2356 + let isSeparator = false; 2357 + if (!isCover) { 2358 + try { 2359 + isSeparator = await isSeparatorImage(dataUrl, dims.width, dims.height, filename); 2360 + } catch (e) { 2361 + console.warn('Separator check failed for', filename, e); 2362 + } 2363 + } 2364 + 2365 + // Tiny images (<200x200) are locked like separators 2366 + const isTiny = (dims.width < 200 && dims.height < 200); 2367 + 2368 + // Images that fit screen can only rotate, not split 2369 + const fitsScreen = (dims.width <= 480 && dims.height <= 800); 2370 + 2371 + // Split capability - no upscaling allowed 2372 + // H-Split scales width to 800, so needs width >= 800 2373 + // V-Split scales height to 800, so needs height >= 800 2374 + const canHSplit = dims.width >= 800; 2375 + const canVSplit = dims.height >= 800; 2376 + 2377 + images.push({ 2378 + path: path, 2379 + name: filename, 2380 + dataUrl: dataUrl, 2381 + width: dims.width, 2382 + height: dims.height, 2383 + isCover: isCover, 2384 + isSeparator: isSeparator || isTiny, 2385 + fitsScreen: fitsScreen, 2386 + canHSplit: canHSplit, 2387 + canVSplit: canVSplit 2388 + }); 2389 + } 2390 + 2391 + return images; 2392 + } 2393 + 2394 + /** 2395 + * Get image dimensions from array buffer 2396 + */ 2397 + function getImageDimensions(data) { 2398 + return new Promise((resolve, reject) => { 2399 + const url = URL.createObjectURL(new Blob([data])); 2400 + const img = new Image(); 2401 + img.onload = () => { 2402 + URL.revokeObjectURL(url); 2403 + resolve({ width: img.width, height: img.height }); 2404 + }; 2405 + img.onerror = () => { 2406 + URL.revokeObjectURL(url); 2407 + resolve({ width: 0, height: 0 }); 2408 + }; 2409 + img.src = url; 2410 + }); 2411 + } 2412 + 2413 + /** 2414 + * Check if image is a separator/ornament 2415 + * Criteria: small size AND (filename match OR symmetric OR extreme aspect ratio) 2416 + */ 2417 + async function isSeparatorImage(dataUrl, width, height, filename) { 2418 + const MAX_DIMENSION = 150; 2419 + const SYMMETRY_THRESHOLD = 0.85; 2420 + 2421 + // First check: must be small in at least one dimension 2422 + const isSmall = (height < MAX_DIMENSION || width < MAX_DIMENSION); 2423 + if (!isSmall) return false; 2424 + 2425 + // Filename hints (instant match if small + named correctly) 2426 + const separatorNames = ['separator', 'divider', 'ornament', 'break', 'flourish', 'scene', 'divid', 'decor']; 2427 + const lowerName = filename.toLowerCase(); 2428 + if (separatorNames.some(n => lowerName.includes(n))) return true; 2429 + 2430 + // Extreme aspect ratio check (>10:1 or <1:10) - these are definitely separators/lines 2431 + const aspectRatio = width / height; 2432 + if (aspectRatio > 10 || aspectRatio < 0.1) return true; 2433 + 2434 + // Symmetry check (skip for very thin images - too few pixels) 2435 + if (width < 10 || height < 10) return true; // Very small = separator 2436 + 2437 + try { 2438 + const isSymmetric = await checkHorizontalSymmetry(dataUrl, width, height, SYMMETRY_THRESHOLD); 2439 + return isSymmetric; 2440 + } catch (e) { 2441 + console.warn('Symmetry check failed:', e); 2442 + return false; 2443 + } 2444 + } 2445 + 2446 + /** 2447 + * Check horizontal symmetry by comparing left and right halves 2448 + */ 2449 + function checkHorizontalSymmetry(dataUrl, width, height, threshold) { 2450 + return new Promise((resolve) => { 2451 + const img = new Image(); 2452 + img.onload = () => { 2453 + // Use small canvas for performance (max 100px wide) 2454 + const scale = Math.min(1, 100 / width); 2455 + const w = Math.max(2, Math.floor(width * scale)); // Minimum 2px 2456 + const h = Math.max(1, Math.floor(height * scale)); // Minimum 1px 2457 + 2458 + const canvas = document.createElement('canvas'); 2459 + canvas.width = w; 2460 + canvas.height = h; 2461 + const ctx = canvas.getContext('2d'); 2462 + 2463 + // Draw scaled image 2464 + ctx.drawImage(img, 0, 0, w, h); 2465 + const imageData = ctx.getImageData(0, 0, w, h); 2466 + const pixels = imageData.data; 2467 + 2468 + // Compare left half with flipped right half 2469 + const halfW = Math.floor(w / 2); 2470 + let matchingPixels = 0; 2471 + let totalPixels = 0; 2472 + 2473 + for (let y = 0; y < h; y++) { 2474 + for (let x = 0; x < halfW; x++) { 2475 + const leftIdx = (y * w + x) * 4; 2476 + const rightIdx = (y * w + (w - 1 - x)) * 4; 2477 + 2478 + // Compare RGB (ignore alpha) 2479 + const rDiff = Math.abs(pixels[leftIdx] - pixels[rightIdx]); 2480 + const gDiff = Math.abs(pixels[leftIdx + 1] - pixels[rightIdx + 1]); 2481 + const bDiff = Math.abs(pixels[leftIdx + 2] - pixels[rightIdx + 2]); 2482 + 2483 + // Allow some tolerance for JPEG artifacts (threshold of 30) 2484 + if (rDiff < 30 && gDiff < 30 && bDiff < 30) { 2485 + matchingPixels++; 2486 + } 2487 + totalPixels++; 2488 + } 2489 + } 2490 + 2491 + const symmetryScore = matchingPixels / totalPixels; 2492 + resolve(symmetryScore >= threshold); 2493 + }; 2494 + img.onerror = () => resolve(false); 2495 + img.src = dataUrl; 2496 + }); 2497 + } 2498 + 2499 + /** 2500 + * Show image picker after EPUB file selection 2501 + */ 2502 + async function showImagePicker(file) { 2503 + // Check if JSZip is available 2504 + if (typeof JSZip === 'undefined') { 2505 + console.error('JSZip not loaded'); 2506 + alert('JSZip library not available. Conversion will proceed without image picker.'); 2507 + startConversionWithImageStates(); 2508 + return; 2509 + } 2510 + 2511 + const pickerSection = document.getElementById('imagePickerSection'); 2512 + const imageGrid = document.getElementById('imageGrid'); 2513 + const countDisplay = document.getElementById('imagePickerCount'); 2514 + 2515 + // Reset state 2516 + imageStates = {}; 2517 + epubImagesCache = []; 2518 + pendingConversionFile = file; 2519 + 2520 + // Extract images 2521 + try { 2522 + const images = await extractImagesForPreview(file); 2523 + epubImagesCache = images; 2524 + 2525 + // Initialize all states to 0 (Normal) 2526 + images.forEach(img => { 2527 + imageStates[img.path] = 0; 2528 + }); 2529 + 2530 + // Build UI 2531 + renderImageGrid(); 2532 + 2533 + // Count images - covers and separators are locked 2534 + const coverCount = images.filter(img => img.isCover).length; 2535 + const separatorCount = images.filter(img => img.isSeparator).length; 2536 + const lockedCount = coverCount + separatorCount; 2537 + const selectableCount = images.length - lockedCount; 2538 + 2539 + if (lockedCount > 0) { 2540 + const lockedParts = []; 2541 + if (coverCount > 0) lockedParts.push(`${coverCount} cover`); 2542 + if (separatorCount > 0) lockedParts.push(`${separatorCount} separator${separatorCount !== 1 ? 's' : ''}`); 2543 + countDisplay.textContent = `${images.length} images (${selectableCount} selectable, ${lockedParts.join(', ')})`; 2544 + } else { 2545 + countDisplay.textContent = `${images.length} image${images.length !== 1 ? 's' : ''} (all selectable)`; 2546 + } 2547 + 2548 + // Show picker, hide upload button, show start conversion button 2549 + pickerSection.style.display = 'block'; 2550 + document.getElementById('uploadBtn').style.display = 'none'; 2551 + document.getElementById('startConversionBtn').style.display = 'block'; 2552 + // Enable two-column layout 2553 + document.querySelector('#uploadModal .modal').classList.add('picker-mode'); 2554 + document.getElementById('pickerColumns').classList.add('picker-active'); 2555 + 2556 + } catch (error) { 2557 + console.error('Failed to extract images:', error); 2558 + alert('Failed to preview images: ' + error.message + '\n\nConversion will proceed normally.'); 2559 + // Fallback: start conversion directly 2560 + startConversionWithImageStates(); 2561 + } 2562 + } 2563 + 2564 + /** 2565 + * Render the image grid with current states 2566 + */ 2567 + function renderImageGrid() { 2568 + const imageGrid = document.getElementById('imageGrid'); 2569 + imageGrid.innerHTML = ''; 2570 + 2571 + const stateLabels = ['', 'H-Split', 'V-Split', 'Rotate']; 2572 + const stateClasses = ['state-0', 'state-1', 'state-2', 'state-3']; 2573 + 2574 + epubImagesCache.forEach(img => { 2575 + // Cover images and separators are always locked (no splitting/rotation) 2576 + const isCover = img.isCover; 2577 + const isSeparator = img.isSeparator; 2578 + const state = imageStates[img.path] || 0; 2579 + 2580 + const item = document.createElement('div'); 2581 + 2582 + if (isCover) { 2583 + // Cover image - locked, cannot be split 2584 + item.className = 'image-item cover-locked'; 2585 + item.title = `${img.width}×${img.height} - Cover image (locked)`; 2586 + item.innerHTML = ` 2587 + <span class="image-state-badge cover-badge">🔒</span> 2588 + <img src="${img.dataUrl}" alt="${img.name}" loading="lazy"> 2589 + <div class="image-name">${img.name}</div> 2590 + `; 2591 + } else if (isSeparator) { 2592 + // Separator/ornament - locked, cannot be split 2593 + item.className = 'image-item separator-locked'; 2594 + item.title = `${img.width}×${img.height} - Separator (locked)`; 2595 + item.innerHTML = ` 2596 + <span class="image-state-badge separator-badge">✦</span> 2597 + <img src="${img.dataUrl}" alt="${img.name}" loading="lazy"> 2598 + <div class="image-name">${img.name}</div> 2599 + `; 2600 + } else { 2601 + // All other images are selectable - all modes allowed 2602 + 2603 + // Determine which overlay to show based on state 2604 + const showRotation = (state === 1 || state === 3); // H-Split or Rotate 2605 + const showSplitLines = (state === 1 || state === 2); // H-Split or V-Split 2606 + const rotateClass = showRotation ? (HANDEDNESS === 'right' ? 'rotate-cw' : 'rotate-ccw') : ''; 2607 + 2608 + // Calculate actual number of parts for split preview 2609 + let numParts = 1; 2610 + if (showSplitLines) { 2611 + let finalWidth; 2612 + if (state === 1) { 2613 + // H-Split: scale width to 800, rotate, then check width 2614 + const scaledH = Math.round(img.height * (800 / img.width)); 2615 + finalWidth = scaledH; // After rotation, height becomes width 2616 + } else { 2617 + // V-Split: scale height to 800, then check width 2618 + finalWidth = Math.round(img.width * (800 / img.height)); 2619 + } 2620 + if (finalWidth > 480) { 2621 + const minOverlapPx = Math.round(480 * (OVERLAP_PERCENT / 100)); 2622 + const maxStep = 480 - minOverlapPx; 2623 + numParts = Math.ceil((finalWidth - minOverlapPx) / maxStep); 2624 + if (numParts < 2) numParts = 2; 2625 + } 2626 + } 2627 + 2628 + // Generate split line elements (numParts - 1 lines at evenly distributed positions) 2629 + let splitLinesHtml = ''; 2630 + if (showSplitLines && numParts > 1) { 2631 + const lines = []; 2632 + const splitClass = state === 1 ? 'split-h' : 'split-v'; 2633 + for (let i = 1; i < numParts; i++) { 2634 + const pos = (i / numParts) * 100; 2635 + lines.push(`<div class="split-line ${splitClass}" style="left:${pos}%"></div>`); 2636 + } 2637 + splitLinesHtml = `<div class="split-lines">${lines.join('')}</div>`; 2638 + } 2639 + 2640 + // Build tooltip 2641 + const stateText = stateLabels[state] || 'Normal'; 2642 + const partsText = numParts > 1 ? ` (${numParts} parts)` : ''; 2643 + 2644 + item.className = `image-item ${stateClasses[state]} ${rotateClass}`.trim(); 2645 + item.onclick = () => cycleImageState(img.path); 2646 + item.title = `${img.width}×${img.height} - ${stateText}${partsText}`; 2647 + item.innerHTML = ` 2648 + <span class="image-state-badge">${stateLabels[state] || '•'}</span> 2649 + <div class="image-preview-overlay"> 2650 + ${splitLinesHtml} 2651 + </div> 2652 + <img src="${img.dataUrl}" alt="${img.name}" loading="lazy"> 2653 + <div class="image-name">${img.name}</div> 2654 + `; 2655 + } 2656 + 2657 + imageGrid.appendChild(item); 2658 + }); 2659 + } 2660 + 2661 + /** 2662 + * Cycle state for a single image 2663 + * All non-locked images: 0 -> 1 -> 2 -> 3 -> 0 2664 + */ 2665 + function cycleImageState(imagePath) { 2666 + const currentState = imageStates[imagePath] || 0; 2667 + imageStates[imagePath] = (currentState + 1) % 4; 2668 + renderImageGrid(); 2669 + } 2670 + 2671 + /** 2672 + * Apply state to all eligible images based on smart rules 2673 + * 0 = Normal (all selectable images) 2674 + * 1 = H-Split (landscapes that canHSplit) 2675 + * 2 = V-Split (all images that canVSplit - portraits AND landscapes) 2676 + * 3 = Rotate (landscapes that don't fit screen) 2677 + */ 2678 + function applyStateToAll(state) { 2679 + epubImagesCache.forEach(img => { 2680 + // Skip locked images 2681 + if (img.isCover || img.isSeparator) return; 2682 + 2683 + const canHSplit = img.canHSplit && !img.fitsScreen; 2684 + const canVSplit = img.canVSplit && !img.fitsScreen; 2685 + const isLandscape = img.width > img.height; 2686 + 2687 + if (state === 0) { 2688 + // Normal - applies to all selectable 2689 + imageStates[img.path] = 0; 2690 + } else if (state === 1) { 2691 + // H-Split - all landscapes that can H-Split 2692 + if (isLandscape && canHSplit) { 2693 + imageStates[img.path] = 1; 2694 + } 2695 + } else if (state === 2) { 2696 + // V-Split - all images that can V-Split (portrait and landscape) 2697 + if (canVSplit) { 2698 + imageStates[img.path] = 2; 2699 + } 2700 + } else if (state === 3) { 2701 + // Rotate - landscapes that exceed screen 2702 + if (isLandscape && !img.fitsScreen) { 2703 + imageStates[img.path] = 3; 2704 + } 2705 + } 2706 + }); 2707 + renderImageGrid(); 2708 + } 2709 + 2710 + /** 2711 + * Start conversion with configured image states 2712 + */ 2713 + function startConversionWithImageStates() { 2714 + const pickerSection = document.getElementById('imagePickerSection'); 2715 + const uploadBtn = document.getElementById('uploadBtn'); 2716 + const startConversionBtn = document.getElementById('startConversionBtn'); 2717 + 2718 + // Hide picker and start conversion button, remove two-column layout 2719 + pickerSection.style.display = 'none'; 2720 + startConversionBtn.style.display = 'none'; 2721 + document.querySelector('#uploadModal .modal').classList.remove('picker-mode'); 2722 + document.getElementById('pickerColumns').classList.remove('picker-active'); 2723 + 2724 + // Show upload button and trigger upload 2725 + uploadBtn.style.display = 'block'; 2726 + uploadBtn.disabled = false; 2727 + uploadFile(); 2728 + } 2729 + 2730 + /** 2731 + * Get processing state for an image path 2732 + * Returns 0 (Normal), 1 (H-Split), 2 (V-Split), or 3 (Rotate) 2733 + */ 2734 + function getImageState(imagePath) { 2735 + return imageStates[imagePath] || 0; 2736 + } 2737 + 2738 + /** 2739 + * Get state label for logging 2740 + */ 2741 + function getStateLabel(state) { 2742 + const labels = ['Normal', 'H-Split', 'V-Split', 'Rotate']; 2743 + return labels[state] || 'Normal'; 2744 + } 2745 + 2746 + // Initialize quality settings handlers 2747 + document.addEventListener('DOMContentLoaded', function() { 2748 + const qualitySlider = document.getElementById('qualitySlider'); 2749 + const qualityInput = document.getElementById('qualityInput'); 2750 + 2751 + // Initialize advanced mode toggle state based on Optimize EPUB checkbox 2752 + const convertCheckbox = document.getElementById('convertBeforeUpload'); 2753 + const advancedToggle = document.getElementById('advancedOptionsToggle'); 2754 + if (convertCheckbox && advancedToggle) { 2755 + const isOptimizeEnabled = convertCheckbox.checked; 2756 + advancedToggle.style.opacity = isOptimizeEnabled ? '1' : '0.5'; 2757 + advancedToggle.style.pointerEvents = isOptimizeEnabled ? 'auto' : 'none'; 2758 + } 2759 + 2760 + if (qualitySlider && qualityInput) { 2761 + // Initialize converter variables with UI default values 2762 + updateQualitySettings(); 2763 + 2764 + // Deselect all presets when slider is manually changed 2765 + const deselectPresets = function() { 2766 + document.querySelectorAll('.quality-preset').forEach(btn => { 2767 + btn.classList.remove('active'); 2768 + }); 2769 + }; 2770 + 2771 + qualitySlider.oninput = function() { 2772 + qualityInput.value = this.value; 2773 + deselectPresets(); 2774 + updateQualitySettings(); 2775 + }; 2776 + qualityInput.onchange = function() { 2777 + let v = Math.max(1, Math.min(95, parseInt(this.value, 10) || 85)); 2778 + this.value = v; 2779 + qualitySlider.value = v; 2780 + deselectPresets(); 2781 + updateQualitySettings(); 2782 + }; 2783 + qualityInput.onblur = function() { 2784 + this.value = qualitySlider.value; 2785 + }; 2786 + } 2787 + }); 2788 + 912 2789 function openFolderModal() { 913 2790 document.getElementById('folderPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath; 914 2791 document.getElementById('folderModal').classList.add('open'); 915 2792 document.getElementById('folderName').value = ''; 2793 + setTimeout(() => document.getElementById('folderName').focus(), 50); 2794 + document.getElementById('folderName').onkeydown = (e) => { if (e.key === 'Enter' && e.target.value.trim()) createFolder(); }; 916 2795 } 917 2796 918 2797 function closeFolderModal() { ··· 1005 2884 }); 1006 2885 } 1007 2886 2887 + // Helper to clear image picker state 2888 + function clearImagePicker() { 2889 + epubImagesCache = []; 2890 + imageStates = {}; 2891 + const imageGrid = document.getElementById('imageGrid'); 2892 + if (imageGrid) imageGrid.innerHTML = ''; 2893 + const pickerSection = document.getElementById('imagePickerSection'); 2894 + if (pickerSection) pickerSection.style.display = 'none'; 2895 + // Reset two-column layout 2896 + document.querySelector('#uploadModal .modal')?.classList.remove('picker-mode'); 2897 + document.getElementById('pickerColumns')?.classList.remove('picker-active'); 2898 + // Hide start conversion, show upload 2899 + const startBtn = document.getElementById('startConversionBtn'); 2900 + if (startBtn) startBtn.style.display = 'none'; 2901 + const uploadBtn = document.getElementById('uploadBtn'); 2902 + if (uploadBtn) uploadBtn.style.display = 'block'; 2903 + } 2904 + 2905 + // Set up file input click listener once 2906 + (function setupFileInputListener() { 2907 + const fileInput = document.getElementById('fileInput'); 2908 + if (!fileInput) return; 2909 + 2910 + fileInput.addEventListener('click', function() { 2911 + // Small delay to allow browser to process click before checking files 2912 + setTimeout(() => { 2913 + if (fileInput.files.length === 0) { 2914 + clearImagePicker(); 2915 + document.getElementById('uploadBtn').disabled = true; 2916 + } 2917 + }, 10); 2918 + }); 2919 + })(); 2920 + 1008 2921 function validateFile() { 1009 2922 const fileInput = document.getElementById('fileInput'); 1010 2923 const uploadBtn = document.getElementById('uploadBtn'); 1011 2924 const files = fileInput.files; 1012 - uploadBtn.disabled = !(files.length > 0); 2925 + const convertOptions = document.getElementById('convertOptions'); 2926 + fileInput.classList.toggle('has-files', files.length > 0); 2927 + 2928 + // Show/hide convert options based on file selection 2929 + if (files.length > 0) { 2930 + convertOptions.style.display = 'block'; 2931 + } else { 2932 + clearImagePicker(); 2933 + } 2934 + 2935 + if (files.length > 0) { 2936 + // If advanced settings is expanded and single EPUB, show image picker 2937 + const advancedContent = document.getElementById('advancedSettingsContent'); 2938 + const convertEnabled = document.getElementById('convertBeforeUpload').checked; 2939 + if (advancedContent.classList.contains('visible') && files.length === 1 && convertEnabled && files[0].name.toLowerCase().endsWith('.epub')) { 2940 + showImagePicker(files[0]).catch(err => console.error('Image picker error:', err)); 2941 + } 2942 + 2943 + // If multiple files with conversion, inform user about batch mode 2944 + if (files.length > 1 && convertEnabled) { 2945 + const epubCount = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.epub')).length; 2946 + if (epubCount > 0) { 2947 + console.log(`Batch mode: ${epubCount} EPUB(s) will use auto settings`); 2948 + } 2949 + } 2950 + 2951 + updateBatchModeUI(files.length > 1); 2952 + uploadBtn.disabled = false; 2953 + } else { 2954 + updateBatchModeUI(false); 2955 + uploadBtn.disabled = true; 2956 + } 1013 2957 } 1014 2958 1015 2959 let failedUploadsGlobal = []; 1016 2960 let wsConnection = null; 2961 + let isUploadInProgress = false; // Prevent modal close during upload/conversion 2962 + let operationCancelled = false; // Set by Cancel to stop conversion loops and upload async handlers 2963 + let uploadGeneration = 0; // Incremented each uploadFile() call; guards stale restoreAfterCancel() 2964 + let currentUploadWs = null; // Active WebSocket reference for external abort 2965 + let currentUploadXhr = null; // Active XHR reference for external abort 1017 2966 const WS_PORT = 81; 1018 2967 const WS_CHUNK_SIZE = 4096; // 4KB chunks - smaller for ESP32 stability 1019 2968 2969 + // ============================================================================ 2970 + // EPUB Image Conversion Functions (from Baseline JPEG Converter) 2971 + // ============================================================================ 2972 + 2973 + // Default conversion settings 2974 + const DEFAULT_MAX_WIDTH = 480; 2975 + const DEFAULT_MAX_HEIGHT = 800; 2976 + const DEFAULT_JPEG_QUALITY = 85; 2977 + const DEFAULT_ENABLE_GRAYSCALE = true; 2978 + // Note: Overlap is now always centered distribution (min 5%) 2979 + 2980 + // Dynamic conversion settings (updated by UI) 2981 + let MAX_WIDTH = DEFAULT_MAX_WIDTH; 2982 + let MAX_HEIGHT = DEFAULT_MAX_HEIGHT; 2983 + let JPEG_QUALITY = DEFAULT_JPEG_QUALITY; 2984 + let ENABLE_GRAYSCALE = DEFAULT_ENABLE_GRAYSCALE; 2985 + let HANDEDNESS = 'right'; // 'right' = clockwise (right-handed), 'left' = counter-clockwise (left-handed) 2986 + let OVERLAP_PERCENT = 5; // Minimum overlap percentage for splits (5%, 10%, 15%) 2987 + 2988 + // ============================================================================ 2989 + // Image Picker State Management 2990 + // ============================================================================ 2991 + 2992 + let imageStates = {}; // Map: imagePath -> state (0=Normal, 1=H-Split, 2=V-Split, 3=Rotate) 2993 + let epubImagesCache = []; // Cache of extracted images for preview 2994 + let pendingConversionFile = null; // File awaiting conversion after image selection 2995 + 2996 + // ============================================================================ 2997 + // Enhanced Logging System 2998 + // ============================================================================ 2999 + 3000 + let logStartTime = null; 3001 + let conversionStats = { images: 0, splits: 0, splitParts: 0, fixes: 0, skipped: 0, errors: 0, originalSize: 0, newSize: 0 }; 3002 + const logSection = document.getElementById('log-section'); 3003 + const logContainer = document.getElementById('log-container'); 3004 + const exportLogCheckbox = document.getElementById('export-log-checkbox'); 3005 + 3006 + // CrossPoint version (fetched from API) 3007 + let crosspointVersion = 'Unknown'; 3008 + 3009 + // Fetch version from API 3010 + async function fetchVersion() { 3011 + try { 3012 + const response = await fetch('/api/status'); 3013 + if (response.ok) { 3014 + const data = await response.json(); 3015 + crosspointVersion = data.version || 'Unknown'; 3016 + } 3017 + } catch (e) { 3018 + console.error('Failed to fetch version:', e); 3019 + } 3020 + } 3021 + 3022 + // Batch logging system for multiple files 3023 + let batchLogEntries = []; 3024 + let batchStats = { filesProcessed: 0, filesSucceeded: 0, filesFailed: 0, totalImages: 0, totalSplits: 0, totalFixes: 0, totalErrors: 0, totalOriginalSize: 0, totalNewSize: 0 }; 3025 + let batchStartTime = null; 3026 + let isBatchMode = false; 3027 + 3028 + // Format bytes to human-readable size (for logging) 3029 + function formatBytes(b) { 3030 + if (!b) return '0 B'; 3031 + const k = 1024; 3032 + const s = ['B', 'KB', 'MB', 'GB']; 3033 + const i = Math.floor(Math.log(b) / Math.log(k)); 3034 + return (b / Math.pow(k, i)).toFixed(1) + ' ' + s[i]; 3035 + } 3036 + 3037 + // Get elapsed timestamp since log start 3038 + function getTimestamp() { 3039 + if (!logStartTime) return '[00:00.0]'; 3040 + const elapsed = (Date.now() - logStartTime) / 1000; 3041 + const mins = Math.floor(elapsed / 60).toString().padStart(2, '0'); 3042 + const secs = (elapsed % 60).toFixed(1).padStart(4, '0'); 3043 + return `[${mins}:${secs}]`; 3044 + } 3045 + 3046 + // Main logging function 3047 + function log(message, type = '', tag = '') { 3048 + const entry = document.createElement('div'); 3049 + entry.className = 'log-entry ' + type; 3050 + 3051 + // Timestamp 3052 + const timestamp = document.createElement('span'); 3053 + timestamp.className = 'log-timestamp'; 3054 + timestamp.textContent = getTimestamp(); 3055 + entry.appendChild(timestamp); 3056 + 3057 + // Tag (if provided) 3058 + if (tag) { 3059 + const tagEl = document.createElement('span'); 3060 + tagEl.className = 'log-tag ' + tag.toLowerCase(); 3061 + tagEl.textContent = tag; 3062 + entry.appendChild(tagEl); 3063 + } 3064 + 3065 + // Message 3066 + const msg = document.createElement('span'); 3067 + msg.className = 'log-message'; 3068 + msg.innerHTML = message; 3069 + entry.appendChild(msg); 3070 + 3071 + logContainer.appendChild(entry); 3072 + logContainer.scrollTop = logContainer.scrollHeight; 3073 + } 3074 + 3075 + // Log image processing details 3076 + function logImage(name, origW, origH, origFormat, origSize, newW, newH, newSize, wasSplit = false, splitCount = 0, partsInfo = null, imageState = 0) { 3077 + const saved = origSize - newSize; 3078 + const savedPct = ((saved / origSize) * 100).toFixed(0); 3079 + const dims = `${origW}×${origH}`; 3080 + const newDims = `${newW}×${newH}`; 3081 + 3082 + // Get state label and color 3083 + const stateLabels = ['', 'H-Split', 'V-Split', 'Rotate']; 3084 + const stateColors = ['', '#3498db', '#e74c3c', '#9b59b6']; 3085 + const stateLabel = stateLabels[imageState] || ''; 3086 + const stateColor = stateColors[imageState] || ''; 3087 + 3088 + if (wasSplit) { 3089 + conversionStats.splits++; 3090 + conversionStats.splitParts += splitCount; 3091 + // Build parts detail string 3092 + let partsDetail = ''; 3093 + if (partsInfo && partsInfo.length > 0) { 3094 + const baseName = name.replace(/\.[^.]+$/, ''); 3095 + partsDetail = partsInfo.map(p => 3096 + `${baseName}${p.suffix}.jpg (${p.width}×${p.height}, ${formatBytes(p.size)})` 3097 + ).join(', '); 3098 + } 3099 + const savedInfo = saved > 0 ? `, <span style="color:#27ae60">-${savedPct}%</span>` : ''; 3100 + const stateIndicator = imageState > 0 ? ` <span style="color:${stateColor};font-weight:600;">[${stateLabel}]</span>` : ''; 3101 + log(`<strong>${escapeHtml(name)}</strong>${stateIndicator} <span class="log-detail">(${dims} ${origFormat.toUpperCase()}, ${formatBytes(origSize)}) → ${splitCount} parts${savedInfo}</span>`, '', 'SPLIT'); 3102 + if (partsDetail) { 3103 + log(`<span class="log-detail" style="margin-left: 20px;">↳ ${partsDetail}</span>`, '', ''); 3104 + } 3105 + } else { 3106 + conversionStats.images++; 3107 + const stateIndicator = imageState > 0 ? ` <span style="color:${stateColor};font-weight:600;">[${stateLabel}]</span>` : ''; 3108 + const detail = saved > 0 3109 + ? `<span class="log-detail">(${dims} → ${newDims}, ${formatBytes(origSize)} → ${formatBytes(newSize)}, <span style="color:#27ae60">-${savedPct}%</span>)</span>` 3110 + : `<span class="log-detail">(${dims} → ${newDims}, ${formatBytes(newSize)})</span>`; 3111 + log(`<strong>${escapeHtml(name)}</strong>${stateIndicator} ${detail}`, '', 'CONVERT'); 3112 + } 3113 + } 3114 + 3115 + // Log fix applied 3116 + function logFix(type, detail) { 3117 + conversionStats.fixes++; 3118 + log(`${type}: <span class="log-detail">${detail}</span>`, 'success', 'FIX'); 3119 + } 3120 + 3121 + // Log skipped item 3122 + function logSkip(name, reason) { 3123 + conversionStats.skipped++; 3124 + log(`${escapeHtml(name)} <span class="log-detail">(${reason})</span>`, '', 'SKIP'); 3125 + } 3126 + 3127 + // Log error 3128 + function logError(message) { 3129 + conversionStats.errors++; 3130 + log(message, 'error', 'ERROR'); 3131 + } 3132 + 3133 + // Log summary table 3134 + function logSummary(originalSize, newSize, timeElapsed) { 3135 + const saved = originalSize - newSize; 3136 + const savedPct = ((saved / originalSize) * 100).toFixed(1); 3137 + const totalImages = conversionStats.images + conversionStats.splits; 3138 + const totalOutput = conversionStats.images + conversionStats.splitParts; 3139 + 3140 + const summaryHtml = ` 3141 + <div class="log-summary"> 3142 + <div class="log-summary-title">📊 Conversion Summary</div> 3143 + <table class="log-summary-table"> 3144 + <tr><td>Images found</td><td class="highlight">${totalImages}</td></tr> 3145 + <tr><td>Images processed</td><td>${totalOutput}${conversionStats.splitParts > conversionStats.splits ? ` (+${conversionStats.splitParts - conversionStats.splits} from splits)` : ''}</td></tr> 3146 + <tr><td>EPUB repairs</td><td>${conversionStats.fixes > 0 ? conversionStats.fixes + ' fixes applied' : 'None needed'}</td></tr> 3147 + ${conversionStats.errors > 0 ? `<tr><td>Errors</td><td style="color:#e74c3c">${conversionStats.errors}</td></tr>` : ''} 3148 + <tr><td>Original size</td><td>${formatBytes(originalSize)}</td></tr> 3149 + <tr><td>Optimized size</td><td>${formatBytes(newSize)}</td></tr> 3150 + <tr><td>Saved</td><td class="${saved > 0 ? 'saved' : 'increased'}">${saved > 0 ? formatBytes(saved) + ' (' + savedPct + '%)' : '+' + formatBytes(-saved)}</td></tr> 3151 + <tr><td>Time</td><td>${timeElapsed.toFixed(1)}s</td></tr> 3152 + </table> 3153 + </div> 3154 + `; 3155 + logContainer.insertAdjacentHTML('beforeend', summaryHtml); 3156 + logContainer.scrollTop = logContainer.scrollHeight; 3157 + } 3158 + 3159 + // Clear log 3160 + function clearLog() { 3161 + logContainer.innerHTML = ''; 3162 + logStartTime = Date.now(); 3163 + conversionStats = { images: 0, splits: 0, splitParts: 0, fixes: 0, skipped: 0, errors: 0, originalSize: 0, newSize: 0 }; 3164 + } 3165 + 3166 + // Start batch logging mode 3167 + function startBatchLog(fileCount) { 3168 + isBatchMode = true; 3169 + batchStartTime = Date.now(); 3170 + batchLogEntries = []; 3171 + batchStats = { filesProcessed: 0, filesSucceeded: 0, filesFailed: 0, totalImages: 0, totalSplits: 0, totalFixes: 0, totalErrors: 0, totalOriginalSize: 0, totalNewSize: 0 }; 3172 + clearLog(); 3173 + logContainer.innerHTML = ''; // Clear display 3174 + log(`Starting batch conversion: ${fileCount} file(s)`, '', 'INFO'); 3175 + } 3176 + 3177 + // Save current file's log to batch entries 3178 + function saveToFileBatchLog(fileName, succeeded) { 3179 + if (!isBatchMode) return; 3180 + 3181 + const entries = Array.from(logContainer.querySelectorAll('.log-entry')); 3182 + batchLogEntries.push({ 3183 + fileName: fileName, 3184 + succeeded: succeeded, 3185 + entries: entries, 3186 + stats: { ...conversionStats } 3187 + }); 3188 + 3189 + // Update batch stats 3190 + batchStats.filesProcessed++; 3191 + if (succeeded) { 3192 + batchStats.filesSucceeded++; 3193 + } else { 3194 + batchStats.filesFailed++; 3195 + } 3196 + batchStats.totalImages += conversionStats.images; 3197 + batchStats.totalSplits += conversionStats.splits; 3198 + batchStats.totalFixes += conversionStats.fixes; 3199 + batchStats.totalErrors += conversionStats.errors; 3200 + 3201 + // Clear for next file 3202 + logContainer.innerHTML = ''; 3203 + conversionStats = { images: 0, splits: 0, splitParts: 0, fixes: 0, skipped: 0, errors: 0, originalSize: 0, newSize: 0 }; 3204 + } 3205 + 3206 + // Finalize batch log and export 3207 + function finalizeBatchLog() { 3208 + if (!isBatchMode) return; 3209 + 3210 + const batchTime = (Date.now() - batchStartTime) / 1000; 3211 + 3212 + // Build consolidated log display 3213 + logContainer.innerHTML = ''; 3214 + log(`Starting batch conversion: ${batchStats.filesProcessed} file(s)`, '', 'INFO'); 3215 + 3216 + // Add all file entries 3217 + batchLogEntries.forEach((fileLog, index) => { 3218 + const fileHeader = document.createElement('div'); 3219 + fileHeader.className = 'log-entry'; 3220 + fileHeader.style.marginTop = index > 0 ? '15px' : '5px'; 3221 + fileHeader.style.borderTop = index > 0 ? '1px solid var(--border-color)' : 'none'; 3222 + fileHeader.style.paddingTop = index > 0 ? '10px' : '0'; 3223 + fileHeader.innerHTML = `<span class="log-timestamp"></span><span class="log-message"><strong>${escapeHtml(fileLog.fileName)}</strong> — ${fileLog.succeeded ? '<span style="color:#27ae60">✓ Success</span>' : '<span style="color:#e74c3c">✗ Failed</span>'}</span>`; 3224 + logContainer.appendChild(fileHeader); 3225 + 3226 + fileLog.entries.forEach(entry => { 3227 + const clone = entry.cloneNode(true); 3228 + logContainer.appendChild(clone); 3229 + }); 3230 + }); 3231 + 3232 + // Add batch summary 3233 + const batchSummaryHtml = ` 3234 + <div class="log-summary"> 3235 + <div class="log-summary-title">📊 Batch Conversion Summary</div> 3236 + <table class="log-summary-table"> 3237 + <tr><td>Files processed</td><td class="highlight">${batchStats.filesProcessed}</td></tr> 3238 + <tr><td>Successful</td><td style="color:#27ae60">${batchStats.filesSucceeded}</td></tr> 3239 + <tr><td>Failed</td><td style="${batchStats.filesFailed > 0 ? '#e74c3c' : '#7f8c8d'}">${batchStats.filesFailed}</td></tr> 3240 + <tr><td>Total images processed</td><td>${batchStats.totalImages}</td></tr> 3241 + <tr><td>Total splits</td><td>${batchStats.totalSplits}</td></tr> 3242 + <tr><td>Total fixes applied</td><td>${batchStats.totalFixes}</td></tr> 3243 + ${batchStats.totalErrors > 0 ? `<tr><td>Total errors</td><td style="color:#e74c3c">${batchStats.totalErrors}</td></tr>` : ''} 3244 + <tr><td>Total time</td><td>${batchTime.toFixed(1)}s</td></tr> 3245 + </table> 3246 + </div> 3247 + `; 3248 + logContainer.insertAdjacentHTML('beforeend', batchSummaryHtml); 3249 + logContainer.scrollTop = logContainer.scrollHeight; 3250 + 3251 + // Auto-export if checkbox is checked 3252 + if (exportLogCheckbox && exportLogCheckbox.checked) { 3253 + setTimeout(() => { 3254 + exportLogToFile(null, true); // isBatch = true 3255 + }, 200); 3256 + } 3257 + 3258 + // Reset batch mode 3259 + isBatchMode = false; 3260 + batchLogEntries = []; 3261 + } 3262 + 3263 + // Show/hide log section 3264 + function showLog() { 3265 + logSection.classList.add('visible'); 3266 + } 3267 + 3268 + function hideLog() { 3269 + logSection.classList.remove('visible'); 3270 + } 3271 + 3272 + // Generate standardized log filename with date 3273 + function generateLogFilename(isBatch = false) { 3274 + const now = new Date(); 3275 + const date = now.toISOString().split('T')[0]; // YYYY-MM-DD 3276 + const time = now.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS 3277 + const prefix = isBatch ? 'batch' : 'epub'; 3278 + return `${prefix}-conversion-log-${date}_${time}.txt`; 3279 + } 3280 + 3281 + // Export log as text file (can be called automatically) 3282 + function exportLogToFile(filename = null, isBatch = false) { 3283 + // Use standardized filename if none provided 3284 + if (!filename) { 3285 + filename = generateLogFilename(isBatch); 3286 + } 3287 + // Extract text from log entries 3288 + const entries = logContainer.querySelectorAll('.log-entry'); 3289 + let logText = `CrossPoint Reader ${crosspointVersion} - EPUB Conversion Log\n`; 3290 + logText += `Generated: ${new Date().toLocaleString()}\n`; 3291 + logText += `${'='.repeat(60)}\n\n`; 3292 + 3293 + entries.forEach(entry => { 3294 + const timestamp = entry.querySelector('.log-timestamp')?.textContent || ''; 3295 + const tag = entry.querySelector('.log-tag')?.textContent || ''; 3296 + const message = entry.querySelector('.log-message')?.textContent || entry.textContent; 3297 + 3298 + if (tag) { 3299 + logText += `${timestamp} [${tag}] ${message}\n`; 3300 + } else { 3301 + logText += `${timestamp} ${message}\n`; 3302 + } 3303 + }); 3304 + 3305 + // Extract summary table if present 3306 + const summary = logContainer.querySelector('.log-summary'); 3307 + if (summary) { 3308 + logText += `\n${'='.repeat(60)}\n`; 3309 + const title = summary.querySelector('.log-summary-title')?.textContent || 'Summary'; 3310 + logText += `${title}\n`; 3311 + logText += `${'-'.repeat(40)}\n`; 3312 + 3313 + const rows = summary.querySelectorAll('tr'); 3314 + rows.forEach(row => { 3315 + const cells = row.querySelectorAll('td'); 3316 + if (cells.length >= 2) { 3317 + logText += `${cells[0].textContent.padEnd(25)} ${cells[1].textContent}\n`; 3318 + } 3319 + }); 3320 + } 3321 + 3322 + // Create download link 3323 + const blob = new Blob([logText], { type: 'text/plain' }); 3324 + const url = URL.createObjectURL(blob); 3325 + const a = document.createElement('a'); 3326 + a.href = url; 3327 + a.download = filename || `epub-conversion-log-${Date.now()}.txt`; 3328 + document.body.appendChild(a); 3329 + a.click(); 3330 + document.body.removeChild(a); 3331 + URL.revokeObjectURL(url); 3332 + } 3333 + 3334 + // ═══════════════════════════════════════════════════════════════════ 3335 + // EPUB Utilities — ported from EPUB Optimizer Pro 3336 + // ═══════════════════════════════════════════════════════════════════ 3337 + 3338 + /** Defensive CSS injected into every XHTML <head> — prevents e-ink overflow. */ 3339 + const DEFENSIVE_STYLE = '<style type="text/css">img,svg{max-width:100%;height:auto}body{overflow-wrap:break-word}table{max-width:100%;table-layout:fixed}pre,code{white-space:pre-wrap;word-wrap:break-word}*{box-sizing:border-box}</style>'; 3340 + 3341 + /** Escape a string for safe insertion into XML attribute values / text content. */ 3342 + function xmlEscape(str) { 3343 + return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); 3344 + } 3345 + 3346 + function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } 3347 + 3348 + /** 3349 + * Decode a URI-encoded href (e.g., "my%20image.jpg" → "my image.jpg"). 3350 + * Handles double-encoding gracefully. 3351 + */ 3352 + function decodeHref(href) { 3353 + try { return decodeURIComponent(href); } 3354 + catch (e) { return href; } 3355 + } 3356 + 3357 + /** 3358 + * Safely read a text file from the zip, handling BOM and encoding. 3359 + * Strips UTF-8 BOM. Detects encoding from XML declaration or meta tag. 3360 + */ 3361 + async function safeReadText(fileObj) { 3362 + const raw = await fileObj.async('uint8array'); 3363 + 3364 + // Detect and strip UTF-8 BOM (EF BB BF) 3365 + let offset = 0; 3366 + if (raw.length >= 3 && raw[0] === 0xEF && raw[1] === 0xBB && raw[2] === 0xBF) { 3367 + offset = 3; 3368 + } 3369 + 3370 + // Try UTF-8 first (vast majority of EPUBs) 3371 + const utf8 = new TextDecoder('utf-8', { fatal: true }); 3372 + try { 3373 + return utf8.decode(raw.subarray(offset)); 3374 + } catch (e) { /* not valid UTF-8 */ } 3375 + 3376 + // Peek at XML declaration or meta charset for encoding hint 3377 + const ascii = new TextDecoder('ascii', { fatal: false }).decode(raw.subarray(offset, offset + 512)); 3378 + const encodingMatch = ascii.match(/encoding=["']([^"']+)["']/i) || 3379 + ascii.match(/charset=["']?([^"'\s;]+)/i); 3380 + const encoding = encodingMatch ? encodingMatch[1].toLowerCase() : 'windows-1252'; 3381 + 3382 + try { 3383 + return new TextDecoder(encoding, { fatal: false }).decode(raw.subarray(offset)); 3384 + } catch (e) { 3385 + // Last resort: lossy latin1 3386 + return new TextDecoder('iso-8859-1', { fatal: false }).decode(raw.subarray(offset)); 3387 + } 3388 + } 3389 + 3390 + /** 3391 + * Find the canonical OPF path by parsing META-INF/container.xml. 3392 + * Falls back to scanning for any .opf file. 3393 + */ 3394 + async function findOPFPath(zip) { 3395 + try { 3396 + const containerPath = Object.keys(zip.files).find(p => p.toLowerCase() === 'meta-inf/container.xml'); 3397 + if (containerPath) { 3398 + const containerXml = await zip.files[containerPath].async('string'); 3399 + const match = containerXml.match(/<rootfile[^>]+full-path=["']([^"']+)["']/i); 3400 + if (match && zip.files[match[1]]) return match[1]; 3401 + } 3402 + } catch (e) { /* fall through */ } 3403 + let fallback = null; 3404 + zip.forEach(p => { if (!fallback && p.toLowerCase().endsWith('.opf')) fallback = p; }); 3405 + return fallback; 3406 + } 3407 + 3408 + /** 3409 + * Resolve a relative href against a base file path. 3410 + * Handles multiple ../, ./, absolute /, and bare relative paths. 3411 + */ 3412 + function resolvePath(basePath, href) { 3413 + if (href.startsWith('/')) return href.substring(1); 3414 + href = href.replace(/^\.\//, ''); 3415 + const baseDir = basePath.includes('/') ? basePath.substring(0, basePath.lastIndexOf('/')) : ''; 3416 + const baseParts = baseDir ? baseDir.split('/') : []; 3417 + const hrefParts = href.split('/'); 3418 + while (hrefParts.length > 0 && hrefParts[0] === '..') { 3419 + hrefParts.shift(); 3420 + if (baseParts.length > 0) baseParts.pop(); 3421 + } 3422 + const resolved = [...baseParts, ...hrefParts].join('/'); 3423 + return resolved.replace(/\/+/g, '/'); 3424 + } 3425 + 3426 + /** 3427 + * Serialize an XML doc back to string, preserving the original <?xml?> declaration 3428 + * and cleaning up XMLSerializer namespace prefix noise (xmlns:ns0 etc). 3429 + */ 3430 + function safeSerialize(doc, originalContent) { 3431 + let result = new XMLSerializer().serializeToString(doc); 3432 + 3433 + // Restore <?xml?> declaration if original had one 3434 + if (originalContent && /^\s*<\?xml\b/.test(originalContent) && !/^\s*<\?xml\b/.test(result)) { 3435 + const declMatch = originalContent.match(/^\s*(<\?xml[^?]*\?>)/); 3436 + if (declMatch) result = declMatch[1] + '\n' + result; 3437 + } 3438 + 3439 + // Clean up XMLSerializer namespace prefix noise (xmlns:ns0="..." ns0:attr="...") 3440 + result = result.replace(/ xmlns:ns\d+="[^"]*"/g, ''); 3441 + result = result.replace(/ ns\d+:/g, ' '); 3442 + 3443 + return result; 3444 + } 3445 + 3446 + /** 3447 + * Extract main identifier from OPF for NCX sync. DOMParser with regex fallback. 3448 + */ 3449 + function extractIdentifier(opfContent) { 3450 + let mainIdentifier = null; 3451 + try { 3452 + const doc = new DOMParser().parseFromString(opfContent, 'application/xml'); 3453 + if (!doc.querySelector('parsererror')) { 3454 + const pkg = doc.getElementsByTagNameNS('*', 'package')[0]; 3455 + const uid = pkg ? pkg.getAttribute('unique-identifier') : null; 3456 + if (uid) { 3457 + const el = [...doc.getElementsByTagNameNS('*', 'identifier')].find(e => e.getAttribute('id') === uid); 3458 + if (el) mainIdentifier = (el.textContent || '').trim(); 3459 + } 3460 + if (!mainIdentifier) { 3461 + const el = doc.getElementsByTagNameNS('*', 'identifier')[0]; 3462 + if (el) mainIdentifier = (el.textContent || '').trim(); 3463 + } 3464 + } 3465 + } catch (e) { /* fall through to regex */ } 3466 + if (!mainIdentifier) { 3467 + const uniqueIdMatch = opfContent.match(/<(?:\w+:)?package[^>]*unique-identifier=["']([^"']+)["']/i); 3468 + if (uniqueIdMatch) { 3469 + const idRegex = new RegExp(`<dc:identifier[^>]*id=["']${uniqueIdMatch[1]}["'][^>]*>([^<]+)</dc:identifier>`, 'i'); 3470 + const idMatch = opfContent.match(idRegex); 3471 + if (idMatch) mainIdentifier = idMatch[1].trim(); 3472 + } 3473 + if (!mainIdentifier) { 3474 + const firstIdMatch = opfContent.match(/<dc:identifier[^>]*>([^<]+)</i); 3475 + if (firstIdMatch) mainIdentifier = firstIdMatch[1].trim(); 3476 + } 3477 + } 3478 + return mainIdentifier; 3479 + } 3480 + 3481 + /** 3482 + * Sync NCX dtb:uid with the given identifier. DOMParser with regex fallback. 3483 + */ 3484 + function syncNCXIdentifier(ncxText, mainIdentifier) { 3485 + if (!mainIdentifier) return ncxText; 3486 + let t = ncxText; 3487 + try { 3488 + const doc = new DOMParser().parseFromString(t, 'application/xml'); 3489 + if (!doc.querySelector('parsererror')) { 3490 + const meta = [...doc.getElementsByTagNameNS('*', 'meta')].find(m => m.getAttribute('name') === 'dtb:uid'); 3491 + if (meta) { 3492 + meta.setAttribute('content', mainIdentifier); 3493 + t = safeSerialize(doc, ncxText); 3494 + } 3495 + } 3496 + } catch (e) { 3497 + t = t.replace(/<meta\s+name=["']dtb:uid["']\s+content=["'][^"']*["']\s*\/?>/gi, `<meta name="dtb:uid" content="${xmlEscape(mainIdentifier)}"/>`); 3498 + } 3499 + return t; 3500 + } 3501 + 3502 + /** 3503 + * Fix OPF content: fix media-types, strip svg properties, 3504 + * update split image manifest entries, ensure cover meta. 3505 + * DOMParser with regex fallback. 3506 + */ 3507 + function fixOPF(opfText, opfOriginal, opfDir, splitImages = {}) { 3508 + let t = opfText; 3509 + 3510 + try { 3511 + const parser = new DOMParser(); 3512 + const doc = parser.parseFromString(t, 'application/xml'); 3513 + if (doc.querySelector('parsererror')) throw new Error('OPF parse failed'); 3514 + 3515 + const items = [...doc.getElementsByTagNameNS('*', 'item')]; 3516 + const manifestEl = doc.getElementsByTagNameNS('*', 'manifest')[0]; 3517 + 3518 + // Fix media-types for converted images 3519 + for (const item of items) { 3520 + const href = item.getAttribute('href') || ''; 3521 + const type = item.getAttribute('media-type') || ''; 3522 + if (href.endsWith('.jpg') && type.match(/^image\/(png|gif|webp|bmp)$/)) { 3523 + item.setAttribute('media-type', 'image/jpeg'); 3524 + } 3525 + } 3526 + 3527 + // Remove 'svg' from properties 3528 + for (const item of items) { 3529 + const props = item.getAttribute('properties') || ''; 3530 + if (props.includes('svg')) { 3531 + const newProps = props.split(/\s+/).filter(p => p !== 'svg').join(' ').trim(); 3532 + if (newProps) item.setAttribute('properties', newProps); 3533 + else item.removeAttribute('properties'); 3534 + } 3535 + } 3536 + 3537 + // Update split image hrefs and add manifest entries for parts 3538 + for (const [splitKey, splitInfo] of Object.entries(splitImages)) { 3539 + const parts = splitInfo.parts || splitInfo; 3540 + let origHref = opfDir && splitKey.startsWith(opfDir + '/') ? splitKey.substring(opfDir.length + 1) : splitKey; 3541 + const origHrefJpg = origHref.replace(/\.(png|gif|webp|bmp|jpeg)$/i, '.jpg'); 3542 + const part1Href = origHrefJpg.replace(/\.jpg$/i, '_part1.jpg'); 3543 + 3544 + for (const item of items) { 3545 + const h = item.getAttribute('href') || ''; 3546 + if (h === origHref || h === origHrefJpg || decodeHref(h) === origHref || decodeHref(h) === origHrefJpg) { 3547 + item.setAttribute('href', part1Href); 3548 + break; 3549 + } 3550 + } 3551 + 3552 + if (manifestEl) { 3553 + const ns = manifestEl.namespaceURI || 'http://www.idpf.org/2007/opf'; 3554 + for (let j = 1; j < parts.length; j++) { 3555 + const p = parts[j]; 3556 + const href = opfDir && p.path.startsWith(opfDir + '/') ? p.path.substring(opfDir.length + 1) : p.path; 3557 + const newItem = doc.createElementNS(ns, 'item'); 3558 + newItem.setAttribute('id', `img-${p.id}`); 3559 + newItem.setAttribute('href', href); 3560 + newItem.setAttribute('media-type', 'image/jpeg'); 3561 + manifestEl.appendChild(newItem); 3562 + } 3563 + } 3564 + } 3565 + 3566 + t = safeSerialize(doc, opfOriginal); 3567 + } catch (e) { 3568 + // Regex fallback 3569 + t = t.replace(/(<(?:\w+:)?item\b[^>]*href="[^"]+\.jpg"[^>]*)media-type="image\/(png|gif|webp|bmp)"/g, '$1media-type="image/jpeg"'); 3570 + t = t.replace(/(<(?:\w+:)?item\b[^>]*)media-type="image\/(png|gif|webp|bmp)"([^>]*href="[^"]+\.jpg")/g, '$1media-type="image/jpeg"$3'); 3571 + t = t.replace(/\s+svg(?=["'\s>])/g, ''); 3572 + for (const [splitKey, splitInfo] of Object.entries(splitImages)) { 3573 + const parts = splitInfo.parts || splitInfo; 3574 + let origHref = opfDir && splitKey.startsWith(opfDir + '/') ? splitKey.substring(opfDir.length + 1) : splitKey; 3575 + const origHrefJpg = origHref.replace(/\.(png|gif|webp|bmp|jpeg)$/i, '.jpg'); 3576 + const part1Href = origHrefJpg.replace(/\.jpg$/i, '_part1.jpg'); 3577 + const origImgRegex = new RegExp(`(href=["'])(${escapeRegex(origHref)}|${escapeRegex(origHrefJpg)})(["'])`, 'gi'); 3578 + t = t.replace(origImgRegex, `$1${part1Href}$3`); 3579 + let adds = ''; 3580 + for (let j = 1; j < parts.length; j++) { 3581 + const p = parts[j]; 3582 + const href = opfDir && p.path.startsWith(opfDir + '/') ? p.path.substring(opfDir.length + 1) : p.path; 3583 + adds += `<item id="img-${xmlEscape(p.id)}" href="${xmlEscape(href)}" media-type="image/jpeg"/>\n`; 3584 + } 3585 + if (adds && t.includes('</manifest>')) t = t.replace('</manifest>', adds + '</manifest>'); 3586 + } 3587 + } 3588 + 3589 + // Ensure cover meta 3590 + const cm = ensureCoverMeta(t); 3591 + if (cm.fixed) t = cm.o; 3592 + 3593 + return t; 3594 + } 3595 + 3596 + // Fix SVG cover - converts SVG-wrapped covers to plain HTML img tags 3597 + function fixSvgCover(content) { 3598 + const hasSvg = content.includes('<svg') || content.includes('<svg:'); 3599 + if (!hasSvg || !content.includes('xlink:href')) return { c: content, fixed: false, count: 0 }; 3600 + if (!content.includes('calibre:cover') && !content.includes('name="cover"') && !content.includes('<title>Cover</title>')) return { c: content, fixed: false, count: 0 }; 3601 + 3602 + try { 3603 + const parser = new DOMParser(); 3604 + const doc = parser.parseFromString(content, 'application/xhtml+xml'); 3605 + 3606 + if (doc.querySelector('parsererror')) { 3607 + // Fallback to regex 3608 + const m = content.match(/xlink:href=["']([^"']+)["']/); 3609 + if (!m) return { c: content, fixed: false, count: 0 }; 3610 + return { c: `<?xml version="1.0" encoding="utf-8"?> 3611 + <!DOCTYPE html> 3612 + <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en" xml:lang="en"> 3613 + <head><meta content="text/html; charset=UTF-8" http-equiv="default-style"/><title>Cover</title></head> 3614 + <body><section epub:type="cover"><img style="max-width:100%;height:auto" alt="Cover" src="${m[1]}"/></section></body> 3615 + </html>`, fixed: true, count: 1 }; 3616 + } 3617 + 3618 + // Find SVG elements - check both standard and namespaced variants 3619 + let imgHref = null; 3620 + const svgNS = 'http://www.w3.org/2000/svg'; 3621 + const xlinkNS = 'http://www.w3.org/1999/xlink'; 3622 + 3623 + // Try to find all SVG elements 3624 + const svgs = [ 3625 + ...doc.getElementsByTagName('svg'), 3626 + ...doc.getElementsByTagNameNS(svgNS, 'svg'), 3627 + ...doc.getElementsByTagName('svg:svg') 3628 + ]; 3629 + 3630 + for (const svg of svgs) { 3631 + // Find image element inside - try all variants 3632 + const imageEl = svg.getElementsByTagName('image')[0] || 3633 + svg.getElementsByTagNameNS(svgNS, 'image')[0] || 3634 + svg.getElementsByTagName('svg:image')[0]; 3635 + 3636 + if (imageEl) { 3637 + imgHref = imageEl.getAttributeNS(xlinkNS, 'href') || 3638 + imageEl.getAttribute('xlink:href') || 3639 + imageEl.getAttribute('href'); 3640 + if (imgHref) break; 3641 + } 3642 + } 3643 + 3644 + if (!imgHref) { 3645 + // Fallback to regex 3646 + const m = content.match(/xlink:href=["']([^"']+)["']/); 3647 + if (!m) return { c: content, fixed: false, count: 0 }; 3648 + imgHref = m[1]; 3649 + } 3650 + 3651 + return { 3652 + c: `<?xml version="1.0" encoding="utf-8"?> 3653 + <!DOCTYPE html> 3654 + <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en" xml:lang="en"> 3655 + <head><meta content="text/html; charset=UTF-8" http-equiv="default-style"/><title>Cover</title></head> 3656 + <body><section epub:type="cover"><img style="max-width:100%;height:auto" alt="Cover" src="${imgHref}"/></section></body> 3657 + </html>`, 3658 + fixed: true, 3659 + count: 1 3660 + }; 3661 + } catch (e) { 3662 + // Fallback to regex 3663 + const m = content.match(/xlink:href=["']([^"']+)["']/); 3664 + if (!m) return { c: content, fixed: false, count: 0 }; 3665 + return { c: `<?xml version="1.0" encoding="utf-8"?> 3666 + <!DOCTYPE html> 3667 + <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en" xml:lang="en"> 3668 + <head><meta content="text/html; charset=UTF-8" http-equiv="default-style"/><title>Cover</title></head> 3669 + <body><section epub:type="cover"><img style="max-width:100%;height:auto" alt="Cover" src="${m[1]}"/></section></body> 3670 + </html>`, fixed: true, count: 1 }; 3671 + } 3672 + } 3673 + 3674 + // Fix SVG-wrapped images - unwrap SVG and replace with plain img 3675 + function fixSvgWrappedImages(content) { 3676 + const hasSvg = content.includes('<svg') || content.includes('<svg:'); 3677 + if (!hasSvg || !content.includes('xlink:href')) return { c: content, fixed: false, count: 0 }; 3678 + 3679 + try { 3680 + const parser = new DOMParser(); 3681 + const doc = parser.parseFromString(content, 'application/xhtml+xml'); 3682 + 3683 + if (doc.querySelector('parsererror')) { 3684 + // Fallback to regex 3685 + let fixedCount = 0; 3686 + const svgImageRegex = /<(?:svg:)?svg\b[^>]*>[\s\S]*?<(?:svg:)?image\b[^>]*xlink:href=["']([^"']+)["'][^>]*\/?>\s*<\/(?:svg:)?svg>/gi; 3687 + const newContent = content.replace(svgImageRegex, (match, href) => { fixedCount++; return `<img style="max-width:100%;height:auto" src="${href}" alt="" />`; }); 3688 + return { c: newContent, fixed: fixedCount > 0, count: fixedCount }; 3689 + } 3690 + 3691 + const svgNS = 'http://www.w3.org/2000/svg'; 3692 + const xlinkNS = 'http://www.w3.org/1999/xlink'; 3693 + 3694 + const svgElements = [...doc.querySelectorAll('svg'), ...doc.getElementsByTagNameNS(svgNS, 'svg')]; 3695 + const uniqueSvgs = [...new Set(svgElements)]; 3696 + let fixedCount = 0; 3697 + 3698 + for (const svg of uniqueSvgs) { 3699 + const imageEl = svg.querySelector('image[*|href]') || svg.getElementsByTagNameNS(svgNS, 'image')[0] || svg.getElementsByTagNameNS('*', 'image')[0]; 3700 + if (!imageEl) continue; 3701 + const href = imageEl.getAttributeNS(xlinkNS, 'href') || imageEl.getAttribute('xlink:href') || imageEl.getAttribute('href'); 3702 + if (!href) continue; 3703 + const width = imageEl.getAttribute('width') || svg.getAttribute('width'); 3704 + const height = imageEl.getAttribute('height') || svg.getAttribute('height'); 3705 + const img = doc.createElementNS('http://www.w3.org/1999/xhtml', 'img'); 3706 + img.setAttribute('src', href); 3707 + img.setAttribute('alt', ''); 3708 + img.setAttribute('style', 'max-width:100%;height:auto'); 3709 + if (width) img.setAttribute('width', width); 3710 + if (height) img.setAttribute('height', height); 3711 + svg.parentNode.replaceChild(img, svg); 3712 + fixedCount++; 3713 + } 3714 + 3715 + if (fixedCount === 0) return { c: content, fixed: false, count: 0 }; 3716 + return { c: safeSerialize(doc, content), fixed: true, count: fixedCount }; 3717 + 3718 + } catch (e) { 3719 + // Fallback to regex 3720 + let fixedCount = 0; 3721 + const svgImageRegex = /<(?:svg:)?svg\b[^>]*>[\s\S]*?<(?:svg:)?image\b[^>]*xlink:href=["']([^"']+)["'][^>]*\/?>\s*<\/(?:svg:)?svg>/gi; 3722 + const newContent = content.replace(svgImageRegex, (match, href) => { fixedCount++; return `<img style="max-width:100%;height:auto" src="${href}" alt="" />`; }); 3723 + return { c: newContent, fixed: fixedCount > 0, count: fixedCount }; 3724 + } 3725 + } 3726 + 3727 + // Ensure cover meta tag exists in OPF — DOMParser with regex fallback 3728 + function ensureCoverMeta(opfString) { 3729 + try { 3730 + const parser = new DOMParser(); 3731 + const doc = parser.parseFromString(opfString, 'application/xml'); 3732 + if (doc.querySelector('parsererror')) throw new Error('Parse failed'); 3733 + 3734 + // Find cover image id: properties="cover-image", or id/href containing "cover" 3735 + let coverId = null; 3736 + const items = [...doc.getElementsByTagNameNS('*', 'item')]; 3737 + for (const item of items) { 3738 + const props = item.getAttribute('properties') || ''; 3739 + const id = item.getAttribute('id') || ''; 3740 + const type = item.getAttribute('media-type') || ''; 3741 + if (!type.startsWith('image/')) continue; 3742 + if (props.includes('cover-image')) { coverId = id; break; } 3743 + } 3744 + if (!coverId) { 3745 + for (const item of items) { 3746 + const id = item.getAttribute('id') || ''; 3747 + const href = item.getAttribute('href') || ''; 3748 + const type = item.getAttribute('media-type') || ''; 3749 + if (!type.startsWith('image/')) continue; 3750 + if (id.toLowerCase().includes('cover') || href.toLowerCase().includes('cover')) { coverId = id; break; } 3751 + } 3752 + } 3753 + if (!coverId) return { o: opfString, fixed: false }; 3754 + 3755 + // Find or create <meta name="cover" content="..."/> 3756 + const metas = [...doc.getElementsByTagNameNS('*', 'meta')]; 3757 + const coverMeta = metas.find(m => m.getAttribute('name') === 'cover'); 3758 + if (coverMeta) { 3759 + if (coverMeta.getAttribute('content') === coverId) return { o: opfString, fixed: false }; 3760 + coverMeta.setAttribute('content', coverId); 3761 + } else { 3762 + const metadata = doc.getElementsByTagNameNS('*', 'metadata')[0]; 3763 + if (!metadata) return { o: opfString, fixed: false }; 3764 + const ns = metadata.namespaceURI || 'http://www.idpf.org/2007/opf'; 3765 + const newMeta = doc.createElementNS(ns, 'meta'); 3766 + newMeta.setAttribute('name', 'cover'); 3767 + newMeta.setAttribute('content', coverId); 3768 + metadata.appendChild(newMeta); 3769 + } 3770 + return { o: safeSerialize(doc, opfString), fixed: true }; 3771 + } catch (e) { 3772 + // Regex fallback 3773 + return ensureCoverMetaRegex(opfString); 3774 + } 3775 + } 3776 + 3777 + function ensureCoverMetaRegex(o) { 3778 + let coverId = null, m; 3779 + if (!coverId && (m = o.match(/<\w+:?item[^>]+id="([^"]+)"[^>]+properties="[^"]*cover-image[^"]*"/i))) coverId = m[1]; 3780 + if (!coverId && (m = o.match(/<\w+:?item[^>]+properties="[^"]*cover-image[^"]*"[^>]+id="([^"]+)"/i))) coverId = m[1]; 3781 + if (!coverId && (m = o.match(/<\w+:?item[^>]*id="([^"]+)"[^>]*href="[^"]*cover[^"]*"[^>]*media-type="image\//i))) coverId = m[1]; 3782 + if (!coverId && (m = o.match(/<\w+:?item[^>]*href="[^"]*cover[^"]*"[^>]*id="([^"]+)"[^>]*media-type="image\//i))) coverId = m[1]; 3783 + if (!coverId && (m = o.match(/<\w+:?item[^>]*id="([^"]*cover[^"]*)"[^>]*media-type="image\//i))) coverId = m[1]; 3784 + if (!coverId && (m = o.match(/<\w+:?item[^>]*media-type="image\/[^"]*"[^>]*id="([^"]*cover[^"]*)"/i))) coverId = m[1]; 3785 + if (!coverId) return { o, fixed: false }; 3786 + const metaMatch = o.match(/<\w+:?meta\s+name=["']cover["']\s+content=["']([^"']+)["']/i) || o.match(/<\w+:?meta\s+content=["']([^"']+)["']\s+name=["']cover["']/i); 3787 + if (metaMatch) { 3788 + if (metaMatch[1] === coverId && !metaMatch[1].includes('/')) return { o, fixed: false }; 3789 + const esc = xmlEscape(coverId); 3790 + o = o.replace(/<\w+:?meta\s+name=["']cover["']\s+content=["'][^"']+["']\s*\/?>/gi, `<meta name="cover" content="${esc}" />`); 3791 + o = o.replace(/<\w+:?meta\s+content=["'][^"']+["']\s+name=["']cover["']\s*\/?>/gi, `<meta name="cover" content="${esc}" />`); 3792 + return { o, fixed: true }; 3793 + } 3794 + const idx = o.indexOf('</metadata>'); 3795 + if (idx !== -1) return { o: o.substring(0, idx) + ` <meta name="cover" content="${xmlEscape(coverId)}"/>\n </metadata>` + o.substring(idx + 11), fixed: true }; 3796 + return { o, fixed: false }; 3797 + } 3798 + 3799 + // Apply grayscale to canvas image data 3800 + function applyGrayscale(ctx, width, height) { 3801 + if (!ENABLE_GRAYSCALE) return; 3802 + const imageData = ctx.getImageData(0, 0, width, height); 3803 + const data = imageData.data; 3804 + for (let i = 0; i < data.length; i += 4) { 3805 + // Alpha-blend against white background before grayscaling (handles transparent PNGs) 3806 + const a = data[i + 3] / 255; 3807 + const blendedR = data[i] * a + 255 * (1 - a); 3808 + const blendedG = data[i + 1] * a + 255 * (1 - a); 3809 + const blendedB = data[i + 2] * a + 255 * (1 - a); 3810 + const gray = Math.round(blendedR * 0.299 + blendedG * 0.587 + blendedB * 0.114); 3811 + data[i] = gray; data[i + 1] = gray; data[i + 2] = gray; data[i + 3] = 255; 3812 + } 3813 + ctx.putImageData(imageData, 0, 0); 3814 + } 3815 + 3816 + // Process single image - returns array of {data, suffix} objects 3817 + const IMAGE_LOAD_TIMEOUT_MS = 30000; // 30 second timeout for image loading 3818 + async function processImage(data, imageState = 0, imagePath = '') { 3819 + return new Promise((resolve, reject) => { 3820 + const url = URL.createObjectURL(new Blob([data])); 3821 + const img = new Image(); 3822 + const origSize = data.byteLength; 3823 + // Set up timeout to handle cases where image never loads 3824 + const timeoutId = setTimeout(() => { 3825 + URL.revokeObjectURL(url); 3826 + reject(new Error('Image load timeout')); 3827 + }, IMAGE_LOAD_TIMEOUT_MS); 3828 + 3829 + img.onload = async () => { 3830 + clearTimeout(timeoutId); 3831 + URL.revokeObjectURL(url); 3832 + const origW = img.width, origH = img.height; 3833 + 3834 + // imageState: 0=Normal, 1=H-Split (CW/CCW), 2=V-Split, 3=Rotate & Fit 3835 + // ======================================================================== 3836 + // STATE 1: H-Split (Rotate + Split) - EXACT COPY FROM index.html 3837 + // Step 1: Scale WIDTH to 800px (keep aspect ratio) 3838 + // Step 2: Rotate 90° CW or CCW based on HANDEDNESS 3839 + // Step 3: If WIDTH > 480, split vertically with overlap 3840 + // ======================================================================== 3841 + if (imageState === 1) { 3842 + // Step 1: Scale WIDTH to 800 (this is the key difference!) 3843 + const scale = MAX_HEIGHT / origW; // 800 / origW 3844 + const scaledW = MAX_HEIGHT; // 800 3845 + const scaledH = Math.round(origH * scale); 3846 + 3847 + const scaledCanvas = document.createElement('canvas'); 3848 + scaledCanvas.width = scaledW; 3849 + scaledCanvas.height = scaledH; 3850 + const scaledCtx = scaledCanvas.getContext('2d'); 3851 + scaledCtx.imageSmoothingEnabled = true; 3852 + scaledCtx.imageSmoothingQuality = 'high'; 3853 + scaledCtx.fillStyle = '#FFF'; 3854 + scaledCtx.fillRect(0, 0, scaledW, scaledH); 3855 + scaledCtx.drawImage(img, 0, 0, origW, origH, 0, 0, scaledW, scaledH); 3856 + 3857 + // Step 2: Rotate 90° CW or CCW 3858 + const rotW = scaledH; 3859 + const rotH = scaledW; // 800 3860 + 3861 + const rotCanvas = document.createElement('canvas'); 3862 + rotCanvas.width = rotW; 3863 + rotCanvas.height = rotH; 3864 + const rotCtx = rotCanvas.getContext('2d'); 3865 + rotCtx.fillStyle = '#FFF'; 3866 + rotCtx.fillRect(0, 0, rotW, rotH); 3867 + 3868 + const isClockwise = HANDEDNESS === 'right'; 3869 + if (isClockwise) { 3870 + // Rotate 90° CW 3871 + rotCtx.translate(rotW, 0); 3872 + rotCtx.rotate(Math.PI / 2); 3873 + } else { 3874 + // Rotate 90° CCW 3875 + rotCtx.translate(0, rotH); 3876 + rotCtx.rotate(-Math.PI / 2); 3877 + } 3878 + rotCtx.drawImage(scaledCanvas, 0, 0); 3879 + rotCtx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform 3880 + applyGrayscale(rotCtx, rotW, rotH); 3881 + 3882 + // Step 3: If WIDTH > 480, split vertically 3883 + if (rotW <= MAX_WIDTH) { 3884 + const blob = await new Promise(res => rotCanvas.toBlob(res, 'image/jpeg', JPEG_QUALITY / 100)); 3885 + const arrBuf = await blob.arrayBuffer(); 3886 + resolve({ 3887 + parts: [{ data: arrBuf, suffix: '', width: rotW, height: rotH, size: arrBuf.byteLength }], 3888 + meta: { origW, origH, origSize, wasSplit: false, rotated: true, finalW: rotW, finalH: rotH, finalSize: arrBuf.byteLength, imageState: 1 } 3889 + }); 3890 + } else { 3891 + // Split by WIDTH (vertical cuts) - from RIGHT to LEFT for CW, LEFT to RIGHT for CCW 3892 + const parts = []; 3893 + const maxW = MAX_WIDTH; // 480 3894 + 3895 + // Centered distribution: calculate numParts first, then distribute evenly 3896 + let overlapPx, step, numParts; 3897 + const minOverlapPx = Math.round(maxW * (OVERLAP_PERCENT / 100)); // Configurable overlap 3898 + const maxStep = maxW - minOverlapPx; 3899 + numParts = Math.ceil((rotW - minOverlapPx) / maxStep); 3900 + if (numParts < 2) numParts = 2; 3901 + // Now calculate step to distribute evenly 3902 + step = Math.round((rotW - maxW) / (numParts - 1)); 3903 + overlapPx = maxW - step; 3904 + // Ensure minimum overlap 3905 + if (overlapPx < minOverlapPx) { 3906 + overlapPx = minOverlapPx; 3907 + step = maxW - overlapPx; 3908 + } 3909 + 3910 + // Calculate all x positions first to ensure consistency 3911 + const positions = []; 3912 + for (let i = 0; i < numParts; i++) { 3913 + let x; 3914 + if (isClockwise) { 3915 + // CW: right to left - start from right edge 3916 + x = rotW - maxW - (i * step); 3917 + } else { 3918 + // CCW: left to right - start from left edge 3919 + x = i * step; 3920 + } 3921 + // Clamp to valid range 3922 + x = Math.max(0, Math.min(x, rotW - maxW)); 3923 + positions.push(x); 3924 + } 3925 + 3926 + // Ensure first and last positions are at edges 3927 + if (isClockwise) { 3928 + positions[0] = rotW - maxW; // First part at right edge 3929 + positions[numParts - 1] = 0; // Last part at left edge 3930 + } else { 3931 + positions[0] = 0; // First part at left edge 3932 + positions[numParts - 1] = rotW - maxW; // Last part at right edge 3933 + } 3934 + 3935 + for (let i = 0; i < numParts; i++) { 3936 + const x = positions[i]; 3937 + const partW = maxW; // Always full width for consistency 3938 + 3939 + const partCanvas = document.createElement('canvas'); 3940 + partCanvas.width = partW; 3941 + partCanvas.height = rotH; 3942 + const partCtx = partCanvas.getContext('2d'); 3943 + // Clear canvas first 3944 + partCtx.fillStyle = '#FFFFFF'; 3945 + partCtx.fillRect(0, 0, partW, rotH); 3946 + // Draw the slice 3947 + partCtx.drawImage(rotCanvas, x, 0, partW, rotH, 0, 0, partW, rotH); 3948 + 3949 + const blob = await new Promise(res => partCanvas.toBlob(res, 'image/jpeg', JPEG_QUALITY / 100)); 3950 + const arrBuf = await blob.arrayBuffer(); 3951 + parts.push({ data: arrBuf, suffix: `_part${i + 1}`, width: partW, height: rotH, size: arrBuf.byteLength }); 3952 + } 3953 + 3954 + const totalSize = parts.reduce((sum, p) => sum + p.size, 0); 3955 + resolve({ 3956 + parts, 3957 + meta: { origW, origH, origSize, wasSplit: true, splitCount: numParts, rotated: true, finalW: parts[0].width, finalH: parts[0].height, finalSize: totalSize, imageState: 1 } 3958 + }); 3959 + } 3960 + } 3961 + // ======================================================================== 3962 + // STATE 2: V-Split (Vertical Split, no rotation) 3963 + // Step 1: Scale HEIGHT to 800px (up or down) 3964 + // Step 2: If WIDTH > 480, split vertically with overlap 3965 + // ======================================================================== 3966 + else if (imageState === 2) { 3967 + // ALWAYS scale height to 800 (up or down) 3968 + const scale = MAX_HEIGHT / origH; // 800 / origH 3969 + const scaledW = Math.round(origW * scale); 3970 + const scaledH = MAX_HEIGHT; // Always 800 3971 + 3972 + const scaledCanvas = document.createElement('canvas'); 3973 + scaledCanvas.width = scaledW; 3974 + scaledCanvas.height = scaledH; 3975 + const scaledCtx = scaledCanvas.getContext('2d'); 3976 + scaledCtx.imageSmoothingEnabled = true; 3977 + scaledCtx.imageSmoothingQuality = 'high'; 3978 + scaledCtx.fillStyle = '#FFF'; 3979 + scaledCtx.fillRect(0, 0, scaledW, scaledH); 3980 + scaledCtx.drawImage(img, 0, 0, origW, origH, 0, 0, scaledW, scaledH); 3981 + applyGrayscale(scaledCtx, scaledW, scaledH); 3982 + 3983 + // Check if split needed 3984 + if (scaledW <= MAX_WIDTH) { 3985 + const blob = await new Promise(res => scaledCanvas.toBlob(res, 'image/jpeg', JPEG_QUALITY / 100)); 3986 + const arrBuf = await blob.arrayBuffer(); 3987 + resolve({ 3988 + parts: [{ data: arrBuf, suffix: '', width: scaledW, height: scaledH, size: arrBuf.byteLength }], 3989 + meta: { origW, origH, origSize, wasSplit: false, rotated: false, finalW: scaledW, finalH: scaledH, finalSize: arrBuf.byteLength, imageState: 2 } 3990 + }); 3991 + } else { 3992 + // Split by WIDTH (vertical cuts) - LEFT to RIGHT (natural reading order) 3993 + const parts = []; 3994 + const maxW = MAX_WIDTH; 3995 + 3996 + // Centered distribution: calculate numParts first, then distribute evenly 3997 + let overlapPx, step, numParts; 3998 + const minOverlapPx = Math.round(maxW * (OVERLAP_PERCENT / 100)); // Configurable overlap 3999 + const maxStep = maxW - minOverlapPx; 4000 + numParts = Math.ceil((scaledW - minOverlapPx) / maxStep); 4001 + if (numParts < 2) numParts = 2; 4002 + // Now calculate step to distribute evenly 4003 + step = Math.round((scaledW - maxW) / (numParts - 1)); 4004 + overlapPx = maxW - step; 4005 + // Ensure minimum overlap 4006 + if (overlapPx < minOverlapPx) { 4007 + overlapPx = minOverlapPx; 4008 + step = maxW - overlapPx; 4009 + } 4010 + 4011 + // Calculate all x positions first to ensure consistency 4012 + const positions = []; 4013 + for (let i = 0; i < numParts; i++) { 4014 + let x = i * step; 4015 + // Clamp to valid range 4016 + x = Math.max(0, Math.min(x, scaledW - maxW)); 4017 + positions.push(x); 4018 + } 4019 + // Ensure last position is at right edge 4020 + positions[0] = 0; 4021 + positions[numParts - 1] = scaledW - maxW; 4022 + 4023 + for (let i = 0; i < numParts; i++) { 4024 + const x = positions[i]; 4025 + const partW = maxW; // Always full width for consistency 4026 + 4027 + const partCanvas = document.createElement('canvas'); 4028 + partCanvas.width = partW; 4029 + partCanvas.height = scaledH; 4030 + const partCtx = partCanvas.getContext('2d'); 4031 + // Clear canvas first 4032 + partCtx.fillStyle = '#FFFFFF'; 4033 + partCtx.fillRect(0, 0, partW, scaledH); 4034 + // Draw the slice 4035 + partCtx.drawImage(scaledCanvas, x, 0, partW, scaledH, 0, 0, partW, scaledH); 4036 + 4037 + const blob = await new Promise(res => partCanvas.toBlob(res, 'image/jpeg', JPEG_QUALITY / 100)); 4038 + const arrBuf = await blob.arrayBuffer(); 4039 + parts.push({ data: arrBuf, suffix: `_part${i + 1}`, width: partW, height: scaledH, size: arrBuf.byteLength }); 4040 + } 4041 + 4042 + const totalSize = parts.reduce((sum, p) => sum + p.size, 0); 4043 + resolve({ 4044 + parts, 4045 + meta: { origW, origH, origSize, wasSplit: true, splitCount: numParts, rotated: false, finalW: parts[0].width, finalH: parts[0].height, finalSize: totalSize, imageState: 2 } 4046 + }); 4047 + } 4048 + } 4049 + // ======================================================================== 4050 + // STATE 3: Rotate & Fit (Rotate 90°, then scale to fit 480x800, no split) 4051 + // ======================================================================== 4052 + else if (imageState === 3) { 4053 + // Step 1: Rotate 90° based on handedness 4054 + const rotW = origH; 4055 + const rotH = origW; 4056 + 4057 + const rotCanvas = document.createElement('canvas'); 4058 + rotCanvas.width = rotW; 4059 + rotCanvas.height = rotH; 4060 + const rotCtx = rotCanvas.getContext('2d'); 4061 + rotCtx.fillStyle = '#FFF'; 4062 + rotCtx.fillRect(0, 0, rotW, rotH); 4063 + 4064 + const isClockwise = HANDEDNESS === 'right'; 4065 + if (isClockwise) { 4066 + rotCtx.translate(rotW, 0); 4067 + rotCtx.rotate(Math.PI / 2); 4068 + } else { 4069 + rotCtx.translate(0, rotH); 4070 + rotCtx.rotate(-Math.PI / 2); 4071 + } 4072 + rotCtx.drawImage(img, 0, 0); 4073 + rotCtx.setTransform(1, 0, 0, 1, 0, 0); 4074 + 4075 + // Step 2: Scale to fit 480x800 (if needed) 4076 + const fitsInScreen = rotW <= MAX_WIDTH && rotH <= MAX_HEIGHT; 4077 + 4078 + if (fitsInScreen) { 4079 + // Already fits after rotation - just apply grayscale 4080 + applyGrayscale(rotCtx, rotW, rotH); 4081 + const blob = await new Promise(res => rotCanvas.toBlob(res, 'image/jpeg', JPEG_QUALITY / 100)); 4082 + const arrBuf = await blob.arrayBuffer(); 4083 + resolve({ 4084 + parts: [{ data: arrBuf, suffix: '', width: rotW, height: rotH, size: arrBuf.byteLength }], 4085 + meta: { origW, origH, origSize, wasSplit: false, rotated: true, finalW: rotW, finalH: rotH, finalSize: arrBuf.byteLength, imageState: 3 } 4086 + }); 4087 + } else { 4088 + // Scale to fit 480x800 4089 + const scale = Math.min(MAX_WIDTH / rotW, MAX_HEIGHT / rotH); 4090 + const newW = Math.round(rotW * scale); 4091 + const newH = Math.round(rotH * scale); 4092 + 4093 + const scaledCanvas = document.createElement('canvas'); 4094 + scaledCanvas.width = newW; 4095 + scaledCanvas.height = newH; 4096 + const scaledCtx = scaledCanvas.getContext('2d'); 4097 + scaledCtx.imageSmoothingEnabled = true; 4098 + scaledCtx.imageSmoothingQuality = 'high'; 4099 + scaledCtx.fillStyle = '#FFF'; 4100 + scaledCtx.fillRect(0, 0, newW, newH); 4101 + scaledCtx.drawImage(rotCanvas, 0, 0, newW, newH); 4102 + applyGrayscale(scaledCtx, newW, newH); 4103 + 4104 + const blob = await new Promise(res => scaledCanvas.toBlob(res, 'image/jpeg', JPEG_QUALITY / 100)); 4105 + const arrBuf = await blob.arrayBuffer(); 4106 + resolve({ 4107 + parts: [{ data: arrBuf, suffix: '', width: newW, height: newH, size: arrBuf.byteLength }], 4108 + meta: { origW, origH, origSize, wasSplit: false, rotated: true, finalW: newW, finalH: newH, finalSize: arrBuf.byteLength, imageState: 3 } 4109 + }); 4110 + } 4111 + } 4112 + // ======================================================================== 4113 + // STATE 0: Normal processing (scale to fit, no split/rotation) 4114 + // ======================================================================== 4115 + else { 4116 + // Normal processing: check if scaling is needed 4117 + const fitsInScreen = origW <= MAX_WIDTH && origH <= MAX_HEIGHT; 4118 + 4119 + if (fitsInScreen) { 4120 + // Image already fits - just convert to JPEG with grayscale 4121 + const c = document.createElement('canvas'); 4122 + c.width = origW; 4123 + c.height = origH; 4124 + const ctx = c.getContext('2d'); 4125 + ctx.fillStyle = '#FFF'; 4126 + ctx.fillRect(0, 0, origW, origH); 4127 + ctx.drawImage(img, 0, 0); 4128 + applyGrayscale(ctx, origW, origH); 4129 + 4130 + const blob = await new Promise(res => c.toBlob(res, 'image/jpeg', JPEG_QUALITY / 100)); 4131 + const arrBuf = await blob.arrayBuffer(); 4132 + resolve({ 4133 + parts: [{ data: arrBuf, suffix: '', width: origW, height: origH, size: arrBuf.byteLength }], 4134 + meta: { origW, origH, origSize, wasSplit: false, rotated: false, finalW: origW, finalH: origH, finalSize: arrBuf.byteLength, imageState: 0 } 4135 + }); 4136 + } else { 4137 + // Scale to fit 480x800 4138 + const scale = Math.min(MAX_WIDTH / origW, MAX_HEIGHT / origH); 4139 + const newW = Math.round(origW * scale); 4140 + const newH = Math.round(origH * scale); 4141 + 4142 + const c = document.createElement('canvas'); 4143 + c.width = newW; 4144 + c.height = newH; 4145 + const ctx = c.getContext('2d'); 4146 + ctx.imageSmoothingEnabled = true; 4147 + ctx.imageSmoothingQuality = 'high'; 4148 + ctx.fillStyle = '#FFF'; 4149 + ctx.fillRect(0, 0, newW, newH); 4150 + ctx.drawImage(img, 0, 0, newW, newH); 4151 + applyGrayscale(ctx, newW, newH); 4152 + 4153 + const blob = await new Promise(res => c.toBlob(res, 'image/jpeg', JPEG_QUALITY / 100)); 4154 + const arrBuf = await blob.arrayBuffer(); 4155 + resolve({ 4156 + parts: [{ data: arrBuf, suffix: '', width: newW, height: newH, size: arrBuf.byteLength }], 4157 + meta: { origW, origH, origSize, wasSplit: false, rotated: false, finalW: newW, finalH: newH, finalSize: arrBuf.byteLength, imageState: 0 } 4158 + }); 4159 + } 4160 + } 4161 + }; 4162 + img.onerror = () => { 4163 + clearTimeout(timeoutId); 4164 + URL.revokeObjectURL(url); 4165 + reject(new Error('Image load failed')); 4166 + }; 4167 + img.src = url; 4168 + }); 4169 + } 4170 + 4171 + // Convert EPUB file - returns converted blob 4172 + async function convertEpubFile(file, progressCallback) { 4173 + const startTime = Date.now(); 4174 + const originalSize = file.size; 4175 + let totalImageSize = 0; 4176 + let totalNewSize = 0; 4177 + 4178 + // Initialize logging 4179 + clearLog(); 4180 + showLog(); 4181 + log(`<strong>${file.name}</strong> <span class="log-detail">(${formatBytes(originalSize)})</span>`, '', 'INFO'); 4182 + log(`Quality: ${JPEG_QUALITY}% | Overlap: ${OVERLAP_PERCENT}% | Rotation: ${HANDEDNESS === 'right' ? 'CW' : 'CCW'} | Grayscale: ${ENABLE_GRAYSCALE ? 'ON' : 'OFF'}`, '', 'INFO'); 4183 + 4184 + const zip = await JSZip.loadAsync(file); 4185 + const renamed = {}; 4186 + zip.forEach(p => { 4187 + const l = p.toLowerCase(); 4188 + if (l.match(/\.(png|gif|webp|bmp|jpeg)$/)) { 4189 + renamed[p] = p.replace(/\.(png|gif|webp|bmp|jpeg)$/i, '.jpg'); 4190 + } 4191 + }); 4192 + 4193 + const out = new JSZip(); 4194 + const entries = Object.entries(zip.files); 4195 + const splitImages = {}; 4196 + const xhtmlFiles = {}; 4197 + let opfPath = null, opfContent = null; 4198 + let mainIdentifier = null; 4199 + 4200 + // Write mimetype FIRST per EPUB OCF spec 4201 + if (zip.files['mimetype']) { 4202 + const mimetypeData = await zip.files['mimetype'].async('arraybuffer'); 4203 + out.file('mimetype', mimetypeData, { compression: 'STORE', createFolders: false }); 4204 + } 4205 + 4206 + // First pass: process images 4207 + for (let i = 0; i < entries.length; i++) { 4208 + if (operationCancelled) throw new Error('Cancelled by user'); 4209 + const [path, fileObj] = entries[i]; 4210 + if (fileObj.dir || path === 'mimetype') continue; 4211 + const low = path.toLowerCase(); 4212 + 4213 + if (low.match(/\.(png|gif|webp|bmp|jpg|jpeg)$/)) { 4214 + const data = await fileObj.async('arraybuffer'); 4215 + const imageState = getImageState(path); 4216 + 4217 + let result; 4218 + try { 4219 + result = await processImage(data, imageState, path); 4220 + } catch (imageError) { 4221 + // Log error but continue with original image 4222 + console.error(`Failed to process image ${path}:`, imageError); 4223 + log(`Warning: Failed to process ${path.split('/').pop()}, using original`, 'warning', 'IMG-ERR'); 4224 + 4225 + // Use original image data as fallback 4226 + result = { 4227 + parts: [{ 4228 + data: data, 4229 + suffix: '', 4230 + width: 0, 4231 + height: 0, 4232 + size: data.byteLength 4233 + }], 4234 + meta: { 4235 + origW: 0, 4236 + origH: 0, 4237 + origSize: data.byteLength, 4238 + wasSplit: false, 4239 + rotated: false, 4240 + finalW: 0, 4241 + finalH: 0, 4242 + finalSize: data.byteLength, 4243 + imageState: imageState, 4244 + processingError: true 4245 + } 4246 + }; 4247 + } 4248 + 4249 + const parts = result.parts; 4250 + const meta = result.meta; 4251 + 4252 + const baseName = path.replace(/\.[^.]+$/, ''); 4253 + const newExt = '.jpg'; 4254 + 4255 + // Log image processing 4256 + const imgName = path.split('/').pop(); 4257 + const origFormat = path.split('.').pop(); 4258 + logImage(imgName, meta.origW, meta.origH, origFormat, meta.origSize, meta.finalW, meta.finalH, meta.finalSize, meta.wasSplit, meta.splitCount || 0, parts, meta.imageState || 0); 4259 + 4260 + totalImageSize += meta.origSize; 4261 + totalNewSize += meta.finalSize; 4262 + 4263 + if (parts.length === 1 && parts[0].suffix === '') { 4264 + const newPath = renamed[path] || path.replace(/\.[^.]+$/, newExt); 4265 + out.file(newPath, parts[0].data, { compression: 'STORE', createFolders: false }); 4266 + } else { 4267 + // Store with full path for collision prevention, but also keep original filename 4268 + const origName = path.split('/').pop(); 4269 + const origDir = path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : ''; 4270 + 4271 + // Key by full path to avoid collisions 4272 + splitImages[path] = { 4273 + origName: origName, 4274 + origDir: origDir, 4275 + parts: [] 4276 + }; 4277 + 4278 + for (const part of parts) { 4279 + const partName = baseName.split('/').pop() + part.suffix + newExt; 4280 + const partPath = (path.includes('/') ? path.substring(0, path.lastIndexOf('/') + 1) : '') + partName; 4281 + out.file(partPath, part.data, { compression: 'STORE', createFolders: false }); 4282 + // Store metadata for XHTML/OPF updates 4283 + splitImages[path].parts.push({ 4284 + path: partPath, 4285 + imgName: partName, 4286 + id: baseName.split('/').pop() + part.suffix, 4287 + suffix: part.suffix 4288 + }); 4289 + } 4290 + } 4291 + } else if (low.match(/\.(xhtml|html|htm)$/)) { 4292 + xhtmlFiles[path] = await safeReadText(fileObj); 4293 + } else if (low.endsWith('.opf')) { 4294 + opfPath = path; 4295 + opfContent = await safeReadText(fileObj); 4296 + } 4297 + 4298 + if (progressCallback) progressCallback((i / entries.length) * 60); 4299 + } 4300 + 4301 + // Second pass: update XHTML using DOMParser 4302 + for (const [xhtmlPath, content] of Object.entries(xhtmlFiles)) { 4303 + if (operationCancelled) throw new Error('Cancelled by user'); 4304 + let t = content; 4305 + const r = fixSvgCover(t); 4306 + if (r.fixed) { t = r.c; logFix('SVG cover', xhtmlPath.split('/').pop()); } 4307 + 4308 + const r2 = fixSvgWrappedImages(t); 4309 + if (r2.fixed) { t = r2.c; logFix(`SVG images (${r2.count})`, xhtmlPath.split('/').pop()); } 4310 + 4311 + for (const [o, n] of Object.entries(renamed)) { 4312 + t = t.split(o.split('/').pop()).join(n.split('/').pop()); 4313 + } 4314 + 4315 + // Use DOMParser for all img modifications: remove width/height and handle split images 4316 + try { 4317 + const parser = new DOMParser(); 4318 + const doc = parser.parseFromString(t, 'application/xhtml+xml'); 4319 + const parseError = doc.querySelector('parsererror'); 4320 + 4321 + if (!parseError) { 4322 + let modified = false; 4323 + 4324 + // Remove width/height attributes from ALL img tags (dimensions may have changed) 4325 + // This prevents CrossPoint and other readers from using wrong dimensions 4326 + const allImgElements = doc.querySelectorAll('img'); 4327 + for (const img of allImgElements) { 4328 + if (img.hasAttribute('width')) { img.removeAttribute('width'); modified = true; } 4329 + if (img.hasAttribute('height')) { img.removeAttribute('height'); modified = true; } 4330 + } 4331 + 4332 + // Handle split images with path collision prevention 4333 + if (Object.keys(splitImages).length > 0) { 4334 + // Get XHTML directory for resolving relative paths 4335 + const xhtmlDir = xhtmlPath.includes('/') ? xhtmlPath.substring(0, xhtmlPath.lastIndexOf('/')) : ''; 4336 + const rootFolders = ['ops', 'oebps', 'epub', 'content']; 4337 + 4338 + for (const [fullPath, splitInfo] of Object.entries(splitImages)) { 4339 + const origName = splitInfo.origName; 4340 + const origDir = splitInfo.origDir; 4341 + const parts = splitInfo.parts; 4342 + const newName = origName.replace(/\.(png|gif|webp|bmp|jpeg)$/i, '.jpg'); 4343 + 4344 + // Extract immediate parent directory for collision prevention 4345 + const splitDirParts = origDir.split('/').filter(p => p); 4346 + const lastDir = splitDirParts.length > 0 ? splitDirParts[splitDirParts.length - 1].toLowerCase() : null; 4347 + const immediateParent = (lastDir && !rootFolders.includes(lastDir)) ? splitDirParts[splitDirParts.length - 1] : null; 4348 + 4349 + // Get XHTML's parent directory parts for relative path resolution 4350 + const xhtmlDirParts = xhtmlDir.split('/').filter(p => p); 4351 + 4352 + // Find all img elements 4353 + const allImgs = doc.querySelectorAll('img'); 4354 + const matchingImgs = []; 4355 + 4356 + for (const img of allImgs) { 4357 + const src = img.getAttribute('src') || ''; 4358 + const srcParts = src.split('/').filter(p => p && p !== '..' && p !== '.'); 4359 + const srcName = srcParts.pop() || ''; 4360 + 4361 + // Check filename match 4362 + if (srcName !== origName && srcName !== newName) continue; 4363 + 4364 + // Path collision prevention with root folder handling 4365 + if (immediateParent) { 4366 + // Image is in a specific subfolder (not root like OPS/OEBPS) 4367 + if (srcParts.length === 0) { 4368 + // src has no path - check if XHTML is in same folder as image 4369 + const xhtmlLastDir = xhtmlDirParts.length > 0 ? xhtmlDirParts[xhtmlDirParts.length - 1] : null; 4370 + if (xhtmlLastDir !== immediateParent) continue; 4371 + } else { 4372 + // src has path - verify parent directory matches 4373 + if (srcParts[srcParts.length - 1] !== immediateParent) continue; 4374 + } 4375 + } else { 4376 + // Image is in root folder (like OEBPS/cover.jpg) 4377 + // Only match if src has NO subfolder path OR points to root folder 4378 + if (srcParts.length > 0) { 4379 + const srcLastDir = srcParts[srcParts.length - 1].toLowerCase(); 4380 + if (!rootFolders.includes(srcLastDir)) continue; 4381 + } 4382 + } 4383 + 4384 + matchingImgs.push(img); 4385 + } 4386 + 4387 + // Process each matching img — Pro's strip+inject approach 4388 + for (const img of matchingImgs) { 4389 + const src = img.getAttribute('src') || ''; 4390 + 4391 + // Part 1: update src in-place, strip original sizing 4392 + img.setAttribute('src', src.replace(origName, parts[0].imgName).replace(newName, parts[0].imgName)); 4393 + 4394 + if (parts.length > 1) { 4395 + // Strip original width/height/class that were sized for the unsplit image 4396 + img.removeAttribute('width'); 4397 + img.removeAttribute('height'); 4398 + img.removeAttribute('class'); 4399 + img.setAttribute('style', 'max-width:100%;height:auto'); 4400 + 4401 + // Neutralize container height constraints that were sized for the original 4402 + let container = img.parentElement; 4403 + const safeContainers = ['div', 'p', 'figure', 'aside', 'section', 'body']; 4404 + while (container && !safeContainers.includes(container.tagName.toLowerCase())) container = container.parentElement; 4405 + const insertTarget = container || img.parentElement; 4406 + // Strip constraining classes/styles from container — they were for the unsplit image 4407 + if (insertTarget && insertTarget.tagName.toLowerCase() !== 'body') { 4408 + insertTarget.removeAttribute('class'); 4409 + insertTarget.removeAttribute('style'); 4410 + } 4411 + const insertParent = insertTarget.parentElement; 4412 + const insertRef = insertTarget.nextSibling; 4413 + const ns = doc.documentElement.namespaceURI || 'http://www.w3.org/1999/xhtml'; 4414 + 4415 + // Insert new minimal wrappers for parts 2+ in reading order 4416 + for (let pi = 1; pi < parts.length; pi++) { 4417 + const wrapper = doc.createElementNS(ns, 'div'); 4418 + const newImg = doc.createElementNS(ns, 'img'); 4419 + const partSrc = src.replace(origName, parts[pi].imgName).replace(newName, parts[pi].imgName); 4420 + newImg.setAttribute('src', partSrc); 4421 + newImg.setAttribute('alt', ''); 4422 + newImg.setAttribute('style', 'max-width:100%;height:auto'); 4423 + wrapper.appendChild(newImg); 4424 + if (insertRef) insertParent.insertBefore(wrapper, insertRef); 4425 + else insertParent.appendChild(wrapper); 4426 + } 4427 + } 4428 + modified = true; 4429 + } 4430 + } 4431 + } 4432 + 4433 + // Only serialize if we made changes 4434 + if (modified) { 4435 + t = safeSerialize(doc, content); 4436 + } 4437 + } 4438 + } catch (e) { 4439 + console.warn('DOMParser error for', xhtmlPath, e.message); 4440 + } 4441 + 4442 + // Inject universal image constraint — prevents overflow on e-ink displays 4443 + if (t.includes('</head>')) { 4444 + t = t.replace('</head>', DEFENSIVE_STYLE + '</head>'); 4445 + } 4446 + 4447 + out.file(xhtmlPath, t, { compression: 'DEFLATE', compressionOptions: { level: 8 }, createFolders: false }); 4448 + } 4449 + 4450 + // Extract main identifier from OPF using DOMParser with regex fallback 4451 + if (opfContent) { 4452 + mainIdentifier = extractIdentifier(opfContent); 4453 + } 4454 + 4455 + // Third pass: update OPF using fixOPF (DOMParser with regex fallback) 4456 + if (opfContent) { 4457 + let t = opfContent; 4458 + for (const [o, n] of Object.entries(renamed)) { 4459 + t = t.split(o.split('/').pop()).join(n.split('/').pop()); 4460 + } 4461 + const opfDir = opfPath.includes('/') ? opfPath.substring(0, opfPath.lastIndexOf('/')) : ''; 4462 + t = fixOPF(t, opfContent, opfDir, splitImages); 4463 + if (t !== opfContent) logFix('OPF', 'manifest updated'); 4464 + out.file(opfPath, t, { compression: 'DEFLATE', compressionOptions: { level: 8 }, createFolders: false }); 4465 + } 4466 + 4467 + // Copy remaining files 4468 + for (const [path, fileObj] of entries) { 4469 + if (operationCancelled) throw new Error('Cancelled by user'); 4470 + if (fileObj.dir || path === 'mimetype') continue; 4471 + const low = path.toLowerCase(); 4472 + if (low.match(/\.(png|gif|webp|bmp|jpg|jpeg)$/) || low.match(/\.(xhtml|html|htm)$/) || low.endsWith('.opf')) continue; 4473 + 4474 + let data = await fileObj.async('arraybuffer'); 4475 + if (low.endsWith('.css')) { 4476 + let t = await safeReadText(fileObj); 4477 + for (const [o, n] of Object.entries(renamed)) { 4478 + t = t.split(o.split('/').pop()).join(n.split('/').pop()); 4479 + } 4480 + data = new TextEncoder().encode(t); 4481 + } else if (low.endsWith('.ncx')) { 4482 + let t = await safeReadText(fileObj); 4483 + for (const [o, n] of Object.entries(renamed)) { 4484 + t = t.split(o.split('/').pop()).join(n.split('/').pop()); 4485 + } 4486 + const oldT = t; 4487 + t = syncNCXIdentifier(t, mainIdentifier); 4488 + if (t !== oldT) logFix('NCX identifier', 'Synced with OPF'); 4489 + data = new TextEncoder().encode(t); 4490 + } 4491 + out.file(path, data, { compression: 'DEFLATE', compressionOptions: { level: 8 }, createFolders: false }); 4492 + } 4493 + 4494 + if (progressCallback) progressCallback(100); 4495 + 4496 + // Generate final blob 4497 + const newBlob = await out.generateAsync({ type: 'blob', mimeType: 'application/epub+zip' }); 4498 + const newSize = newBlob.size; 4499 + const timeElapsed = (Date.now() - startTime) / 1000; 4500 + 4501 + // Log completion 4502 + log('Conversion complete!', 'success', 'DONE'); 4503 + logSummary(totalImageSize > 0 ? totalImageSize : originalSize, totalNewSize > 0 ? totalNewSize : newSize, timeElapsed); 4504 + 4505 + // Auto-export only if NOT in batch mode (batch mode exports at the end) 4506 + if (!isBatchMode && exportLogCheckbox && exportLogCheckbox.checked) { 4507 + setTimeout(() => { 4508 + exportLogToFile(null, false); // isBatch = false for single file 4509 + }, 100); 4510 + } 4511 + 4512 + return newBlob; 4513 + } 4514 + 1020 4515 // Get WebSocket URL based on current page location 1021 4516 function getWsUrl() { 1022 4517 const host = window.location.hostname; ··· 1027 4522 function uploadFileWebSocket(file, onProgress, onComplete, onError) { 1028 4523 return new Promise((resolve, reject) => { 1029 4524 const ws = new WebSocket(getWsUrl()); 4525 + currentUploadWs = ws; 1030 4526 let uploadStarted = false; 1031 4527 let sendingChunks = false; 4528 + let uploadComplete = false; // set only when DONE is received and resolve() called 1032 4529 1033 4530 ws.binaryType = 'arraybuffer'; 1034 4531 ··· 1071 4568 ws.send(buffer); 1072 4569 offset += chunkSize; 1073 4570 1074 - // Update local progress - cap at 95% since server still needs to write 1075 - // Final 100% shown when server confirms DONE 4571 + // Update local progress with real transfer progress 4572 + // Server will confirm 100% with DONE message 1076 4573 if (onProgress) { 1077 - const cappedOffset = Math.min(offset, Math.floor(totalSize * 0.95)); 1078 - onProgress(cappedOffset, totalSize); 4574 + onProgress(offset, totalSize); 1079 4575 } 1080 4576 } 1081 4577 ··· 1094 4590 } else if (msg === 'DONE') { 1095 4591 // Show 100% when server confirms completion 1096 4592 if (onProgress) onProgress(file.size, file.size); 4593 + uploadComplete = true; 4594 + currentUploadWs = null; 1097 4595 ws.close(); 1098 4596 if (onComplete) onComplete(); 1099 4597 resolve(); ··· 1107 4605 1108 4606 ws.onerror = function(event) { 1109 4607 console.error('[WS] Error:', event); 4608 + currentUploadWs = null; 1110 4609 if (!uploadStarted) { 1111 4610 reject(new Error('WebSocket connection failed')); 1112 4611 } else if (!sendingChunks) { 1113 4612 reject(new Error('WebSocket error during upload')); 4613 + } else { 4614 + // Error during chunk sending - reject with appropriate message 4615 + reject(new Error('WebSocket error during file transfer')); 1114 4616 } 1115 4617 }; 1116 4618 1117 4619 ws.onclose = function(event) { 1118 4620 console.log('[WS] Connection closed, code:', event.code, 'reason:', event.reason); 1119 - if (sendingChunks) { 1120 - reject(new Error('WebSocket closed unexpectedly')); 4621 + // Reject for any close before upload was confirmed complete (covers both 4622 + // mid-chunk-send closes and the "all chunks sent, waiting for DONE" window) 4623 + if (!uploadComplete) { 4624 + reject(new Error('WebSocket closed during upload')); 1121 4625 } 1122 4626 }; 1123 4627 }); ··· 1130 4634 formData.append('file', file); 1131 4635 1132 4636 const xhr = new XMLHttpRequest(); 4637 + currentUploadXhr = xhr; 1133 4638 xhr.open('POST', '/upload?path=' + encodeURIComponent(currentPath), true); 1134 4639 1135 4640 xhr.upload.onprogress = function(e) { ··· 1139 4644 }; 1140 4645 1141 4646 xhr.onload = function() { 4647 + currentUploadXhr = null; 1142 4648 if (xhr.status === 200) { 1143 4649 if (onComplete) onComplete(); 1144 4650 resolve(); ··· 1150 4656 }; 1151 4657 1152 4658 xhr.onerror = function() { 4659 + currentUploadXhr = null; 1153 4660 const error = 'Network error'; 1154 4661 if (onError) onError(error); 1155 4662 reject(new Error(error)); 4663 + }; 4664 + 4665 + xhr.onabort = function() { 4666 + currentUploadXhr = null; 4667 + reject(new Error('Upload aborted')); 1156 4668 }; 1157 4669 1158 4670 xhr.send(formData); ··· 1162 4674 function uploadFile() { 1163 4675 const fileInput = document.getElementById('fileInput'); 1164 4676 const files = Array.from(fileInput.files); 4677 + const convertEnabled = document.getElementById('convertBeforeUpload').checked; 1165 4678 1166 4679 if (files.length === 0) { 1167 4680 alert('Please select at least one file!'); 1168 4681 return; 1169 4682 } 4683 + 4684 + // Prevent modal close during upload 4685 + isUploadInProgress = true; 4686 + uploadGeneration++; 4687 + const myGeneration = uploadGeneration; 4688 + document.getElementById('uploadModalClose').classList.add('disabled'); 1170 4689 1171 4690 const progressContainer = document.getElementById('progress-container'); 1172 4691 const progressFill = document.getElementById('progress-fill'); ··· 1180 4699 const failedFiles = []; 1181 4700 let useWebSocket = true; // Try WebSocket first 1182 4701 4702 + // Check if we should use batch logging mode 4703 + const epubFilesToConvert = files.filter(f => f.name.toLowerCase().endsWith('.epub') && convertEnabled); 4704 + const useBatchLog = epubFilesToConvert.length > 1 && exportLogCheckbox && exportLogCheckbox.checked; 4705 + 4706 + // Start batch log mode if needed 4707 + if (useBatchLog) { 4708 + startBatchLog(epubFilesToConvert.length); 4709 + showLog(); 4710 + } 4711 + 1183 4712 async function uploadNextFile() { 1184 4713 if (currentIndex >= files.length) { 1185 4714 // All files processed - show summary 1186 4715 if (failedFiles.length === 0) { 1187 4716 progressFill.style.backgroundColor = '#4caf50'; 1188 4717 progressText.textContent = 'All uploads complete!'; 1189 - setTimeout(() => { 1190 - closeUploadModal(); 1191 - hydrate(); 1192 - }, 1000); 4718 + 4719 + // Finalize batch log if in batch mode 4720 + if (useBatchLog) { 4721 + finalizeBatchLog(); 4722 + setTimeout(() => { 4723 + window.location.reload(); 4724 + }, 2000); 4725 + } else { 4726 + setTimeout(() => { 4727 + window.location.reload(); 4728 + }, 1000); 4729 + } 1193 4730 } else { 1194 4731 progressFill.style.backgroundColor = '#e74c3c'; 1195 4732 const failedList = failedFiles.map(f => f.name).join(', '); 1196 4733 progressText.textContent = `${files.length - failedFiles.length}/${files.length} uploaded. Failed: ${failedList}`; 1197 - failedUploadsGlobal = failedFiles; 1198 - setTimeout(() => { 4734 + 4735 + // Add upload errors to batch log 4736 + if (useBatchLog) { 4737 + failedFiles.forEach(ff => { 4738 + logError(`Upload failed for ${ff.name}: ${ff.error}`); 4739 + }); 4740 + finalizeBatchLog(); 4741 + } 4742 + 4743 + // Only show banner if THIS upload session had failures 4744 + // Use local failedFiles, not the global shared variable 4745 + if (failedFiles.length > 0) { 4746 + // Accumulate failed uploads to global (don't replace) 4747 + failedUploadsGlobal = failedUploadsGlobal.concat(failedFiles); 4748 + // Clear flag and close modal, then show banner with retry options 4749 + isUploadInProgress = false; 4750 + document.getElementById('uploadModalClose').classList.remove('disabled'); 1199 4751 closeUploadModal(); 1200 4752 showFailedUploadsBanner(); 1201 - hydrate(); 1202 - }, 2000); 4753 + } 1203 4754 } 1204 4755 return; 1205 4756 } 1206 4757 1207 - const file = files[currentIndex]; 4758 + let file = files[currentIndex]; 4759 + const originalFile = file; 4760 + // Reset progress bar instantly without transition when starting a new file 4761 + progressFill.classList.add('no-transition'); 1208 4762 progressFill.style.width = '0%'; 1209 4763 progressFill.style.backgroundColor = '#27ae60'; 4764 + // Re-enable transition after a brief delay 4765 + setTimeout(() => progressFill.classList.remove('no-transition'), 50); 4766 + 4767 + // Check if file is an EPUB and conversion is enabled 4768 + const isEpub = file.name.toLowerCase().endsWith('.epub'); 4769 + const needsConversion = isEpub && convertEnabled; 4770 + let conversionSucceeded = false; 4771 + let conversionFailed = false; // Track if conversion actually failed 4772 + 1210 4773 const methodText = useWebSocket ? ' [WS]' : ' [HTTP]'; 1211 - progressText.textContent = `Uploading ${file.name} (${currentIndex + 1}/${files.length})${methodText}`; 4774 + const stageText = needsConversion ? 'Converting & uploading' : 'Uploading'; 4775 + progressText.style.color = ''; 4776 + progressText.textContent = `${stageText} ${file.name} (${currentIndex + 1}/${files.length})${methodText}`; 1212 4777 1213 4778 const onProgress = (loaded, total) => { 1214 - const percent = Math.round((loaded / total) * 100); 1215 - progressFill.style.width = percent + '%'; 1216 - const speed = ''; // Could calculate speed here 1217 - progressText.textContent = `Uploading ${file.name} (${currentIndex + 1}/${files.length})${methodText} — ${percent}%`; 4779 + const uploadPercent = Math.round((loaded / total) * 100); 4780 + // If conversion succeeded, display goes from 50-100%, otherwise 0-100% 4781 + const displayPercent = conversionSucceeded ? 50 + Math.round(uploadPercent / 2) : uploadPercent; 4782 + progressFill.style.width = displayPercent + '%'; 4783 + const prefix = conversionSucceeded ? 'Converting & uploading' : 'Uploading'; 4784 + progressText.textContent = `${prefix} ${file.name} (${currentIndex + 1}/${files.length})${methodText} — ${uploadPercent}%`; 1218 4785 }; 1219 4786 1220 4787 const onComplete = () => { 4788 + // Save file log to batch if in batch mode and this file was converted 4789 + // Consider it successful only if conversion didn't fail 4790 + if (useBatchLog && needsConversion) { 4791 + saveToFileBatchLog(file.name, !conversionFailed && conversionSucceeded); 4792 + } 4793 + 1221 4794 currentIndex++; 1222 4795 uploadNextFile(); 1223 4796 }; 1224 4797 1225 4798 const onError = (error) => { 1226 - failedFiles.push({ name: file.name, error: error, file: file }); 4799 + // Save failed file log to batch if in batch mode 4800 + if (useBatchLog && needsConversion) { 4801 + logError(`Upload failed: ${error}`); 4802 + saveToFileBatchLog(file.name, false); 4803 + } 4804 + 4805 + failedFiles.push({ name: file.name, error: error, file: originalFile }); 4806 + 4807 + // If network error, mark all remaining files as failed and show retry banner 4808 + if (error.includes('connection failed') || error.includes('Network error') || 4809 + error.includes('timeout') || error.includes('disconnected')) { 4810 + console.log(`[Network] Network error detected: ${error}`); 4811 + 4812 + // Add all remaining files to failed list 4813 + const remainingFiles = files.slice(currentIndex + 1); 4814 + remainingFiles.forEach(remainingFile => { 4815 + failedFiles.push({ 4816 + name: remainingFile.name, 4817 + error: 'Network error - upload interrupted', 4818 + file: remainingFile 4819 + }); 4820 + }); 4821 + 4822 + // Show retry banner immediately with all failed files 4823 + failedUploadsGlobal = failedUploadsGlobal.concat(failedFiles); 4824 + isUploadInProgress = false; 4825 + document.getElementById('uploadModalClose').classList.remove('disabled'); 4826 + closeUploadModal(); 4827 + showFailedUploadsBanner(); 4828 + return; // Stop processing 4829 + } 4830 + 1227 4831 currentIndex++; 1228 4832 uploadNextFile(); 1229 4833 }; 1230 4834 1231 4835 try { 4836 + // Convert EPUB if needed 4837 + if (needsConversion) { 4838 + progressFill.style.backgroundColor = '#9b59b6'; // Purple for conversion 4839 + progressText.textContent = `Converting ${file.name} (${currentIndex + 1}/${files.length})...`; 4840 + 4841 + // Clear log for single file mode, or just add separator for batch mode 4842 + if (!useBatchLog) { 4843 + clearLog(); 4844 + showLog(); 4845 + } 4846 + 4847 + try { 4848 + const convertedBlob = await convertEpubFile(file, (percent) => { 4849 + // Pass current quality setting to converter 4850 + progressFill.style.width = (percent * 0.5) + '%'; // Conversion takes first 50% 4851 + }); 4852 + 4853 + // Create new File from converted blob 4854 + file = new File([convertedBlob], file.name, { type: 'application/epub+zip' }); 4855 + progressFill.style.backgroundColor = '#27ae60'; // Back to green for upload 4856 + conversionSucceeded = true; 4857 + } catch (convError) { 4858 + if (operationCancelled) { if (uploadGeneration === myGeneration) restoreAfterCancel(); return; } 4859 + console.error('Conversion error:', convError); 4860 + // Log the error 4861 + logError(`Conversion failed: ${convError.message}`); 4862 + log('Uploading original file instead...', 'warning', 'INFO'); 4863 + conversionFailed = true; 4864 + 4865 + // In single file mode, export error log 4866 + if (!useBatchLog && exportLogCheckbox && exportLogCheckbox.checked) { 4867 + setTimeout(() => { 4868 + exportLogToFile(null, false); // isBatch = false for single file 4869 + }, 100); 4870 + } 4871 + 4872 + // If conversion fails, try uploading original file 4873 + progressText.textContent = `Conversion failed, uploading original ${file.name}...`; 4874 + progressFill.style.backgroundColor = '#e67e22'; // Orange for fallback 4875 + // Reset progress bar to 0% for original file upload 4876 + progressFill.style.width = '0%'; 4877 + } 4878 + } 4879 + 1232 4880 if (useWebSocket) { 1233 4881 await uploadFileWebSocket(file, onProgress, null, null); 1234 - onComplete(); 1235 4882 } else { 1236 4883 await uploadFileHTTP(file, onProgress, null, null); 1237 - onComplete(); 1238 4884 } 4885 + // Ensure progress bar shows 100% before moving to next file 4886 + progressFill.style.width = '100%'; 4887 + progressText.textContent = `Upload complete: ${file.name}`; 4888 + onComplete(); 1239 4889 } catch (error) { 4890 + if (operationCancelled) { if (uploadGeneration === myGeneration) restoreAfterCancel(); return; } 1240 4891 console.error('Upload error:', error); 4892 + // Log upload error if conversion succeeded but upload failed 4893 + if (conversionSucceeded) { 4894 + logError(`Upload failed: ${error.message}`); 4895 + } 4896 + 1241 4897 if (useWebSocket && error.message === 'WebSocket connection failed') { 1242 4898 // Fall back to HTTP for all subsequent uploads 1243 4899 console.log('WebSocket failed, falling back to HTTP'); ··· 1261 4917 function showFailedUploadsBanner() { 1262 4918 const banner = document.getElementById('failedUploadsBanner'); 1263 4919 const filesList = document.getElementById('failedFilesList'); 1264 - 4920 + 1265 4921 filesList.innerHTML = ''; 1266 - 4922 + 1267 4923 failedUploadsGlobal.forEach((failedFile, index) => { 1268 4924 const item = document.createElement('div'); 1269 4925 item.className = 'failed-file-item'; ··· 1276 4932 `; 1277 4933 filesList.appendChild(item); 1278 4934 }); 1279 - 4935 + 1280 4936 // Ensure retry all button is visible 1281 4937 const retryAllBtn = banner.querySelector('.retry-all-btn'); 1282 4938 if (retryAllBtn) retryAllBtn.style.display = ''; 1283 - 4939 + 1284 4940 banner.classList.add('show'); 1285 4941 } 1286 4942 ··· 1293 4949 function retrySingleUpload(index) { 1294 4950 const failedFile = failedUploadsGlobal[index]; 1295 4951 if (!failedFile) return; 1296 - 4952 + 1297 4953 // Create a DataTransfer to set the file input 1298 4954 const dt = new DataTransfer(); 1299 4955 dt.items.add(failedFile.file); 1300 - 4956 + 1301 4957 const fileInput = document.getElementById('fileInput'); 1302 4958 fileInput.files = dt.files; 1303 - 4959 + 1304 4960 // Remove this file from failed list 1305 4961 failedUploadsGlobal.splice(index, 1); 1306 - 4962 + 1307 4963 // If no more failed files, hide banner 1308 4964 if (failedUploadsGlobal.length === 0) { 1309 4965 dismissFailedUploads(); 1310 4966 } 1311 - 4967 + 1312 4968 // Open modal and trigger upload 1313 4969 openUploadModal(); 1314 4970 validateFile(); ··· 1316 4972 1317 4973 function retryAllFailedUploads() { 1318 4974 if (failedUploadsGlobal.length === 0) return; 1319 - 4975 + 1320 4976 // Create a DataTransfer with all failed files 1321 4977 const dt = new DataTransfer(); 1322 4978 failedUploadsGlobal.forEach(failedFile => { 1323 4979 dt.items.add(failedFile.file); 1324 4980 }); 1325 - 4981 + 1326 4982 const fileInput = document.getElementById('fileInput'); 1327 4983 fileInput.files = dt.files; 1328 - 4984 + 1329 4985 // Clear failed files list 1330 4986 failedUploadsGlobal = []; 1331 4987 dismissFailedUploads(); 1332 - 4988 + 1333 4989 // Open modal and trigger upload 1334 4990 openUploadModal(); 1335 4991 validateFile();
+13
src/network/html/js/jszip.min.js
··· 1 + /*! 2 + 3 + JSZip v3.10.1 - A JavaScript class for generating and reading zip files 4 + <http://stuartk.com/jszip> 5 + 6 + (c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com> 7 + Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown. 8 + 9 + JSZip uses the library pako released under the MIT license : 10 + https://github.com/nodeca/pako/blob/main/LICENSE 11 + */ 12 + 13 + !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta})}},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){"use strict";function A(e,t){var r,n="";for(r=0;r<t;r++)n+=String.fromCharCode(255&e),e>>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}))},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t<this.dirRecords.length;t++)this.push({data:this.dirRecords[t],meta:{percent:100}});var r=this.bytesWritten-e,n=function(e,t,r,n,i){var s=I.transformTo("string",i(n));return R.CENTRAL_DIRECTORY_END+"\0\0\0\0"+A(e,2)+A(e,2)+A(t,4)+A(r,4)+A(s.length,2)+s}(this.dirRecords.length,r,e,this.zipComment,this.encodeFileName);this.push({data:n,meta:{percent:100}})},s.prototype.prepareNextSource=function(){this.previous=this._sources.shift(),this.openedSource(this.previous.streamInfo),this.isPaused?this.previous.pause():this.previous.resume()},s.prototype.registerPrevious=function(e){this._sources.push(e);var t=this;return e.on("data",function(e){t.processChunk(e)}),e.on("end",function(){t.closedSource(t.previous.streamInfo),t._sources.length?t.prepareNextSource():t.end()}),e.on("error",function(e){t.error(e)}),this},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this.previous&&this._sources.length?(this.prepareNextSource(),!0):this.previous||this._sources.length||this.generatedError?void 0:(this.end(),!0))},s.prototype.error=function(e){var t=this._sources;if(!i.prototype.error.call(this,e))return!1;for(var r=0;r<t.length;r++)try{t[r].error(e)}catch(e){}return!0},s.prototype.lock=function(){i.prototype.lock.call(this);for(var e=this._sources,t=0;t<e.length;t++)e[t].lock()},t.exports=s},{"../crc32":4,"../signature":23,"../stream/GenericWorker":28,"../utf8":31,"../utils":32}],9:[function(e,t,r){"use strict";var u=e("../compressions"),n=e("./ZipFileWorker");r.generateWorker=function(e,a,t){var o=new n(a.streamFiles,t,a.platform,a.encodeFileName),h=0;try{e.forEach(function(e,t){h++;var r=function(e,t){var r=e||t,n=u[r];if(!n)throw new Error(r+" is not a valid compression method !");return n}(t.options.compression,a.compression),n=t.options.compressionOptions||a.compressionOptions||{},i=t.dir,s=t.date;t._compressWorker(r,n).withStreamInfo("file",{name:e,dir:i,date:s,comment:t.comment||"",unixPermissions:t.unixPermissions,dosPermissions:t.dosPermissions}).pipe(o)}),o.entriesCount=h}catch(e){o.error(e)}return o}},{"../compressions":3,"./ZipFileWorker":8}],10:[function(e,t,r){"use strict";function n(){if(!(this instanceof n))return new n;if(arguments.length)throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide.");this.files=Object.create(null),this.comment=null,this.root="",this.clone=function(){var e=new n;for(var t in this)"function"!=typeof this[t]&&(e[t]=this[t]);return e}}(n.prototype=e("./object")).loadAsync=e("./load"),n.support=e("./support"),n.defaults=e("./defaults"),n.version="3.10.1",n.loadAsync=function(e,t){return(new n).loadAsync(e,t)},n.external=e("./external"),t.exports=n},{"./defaults":5,"./external":6,"./load":11,"./object":15,"./support":30}],11:[function(e,t,r){"use strict";var u=e("./utils"),i=e("./external"),n=e("./utf8"),s=e("./zipEntries"),a=e("./stream/Crc32Probe"),l=e("./nodejsUtils");function f(n){return new i.Promise(function(e,t){var r=n.decompressed.getContentWorker().pipe(new a);r.on("error",function(e){t(e)}).on("end",function(){r.streamInfo.crc32!==n.decompressed.crc32?t(new Error("Corrupted zip : CRC32 mismatch")):e()}).resume()})}t.exports=function(e,o){var h=this;return o=u.extend(o||{},{base64:!1,checkCRC32:!1,optimizedBinaryString:!1,createFolders:!1,decodeFileName:n.utf8decode}),l.isNode&&l.isStream(e)?i.Promise.reject(new Error("JSZip can't accept a stream when loading a zip file.")):u.prepareContent("the loaded zip file",e,!0,o.optimizedBinaryString,o.base64).then(function(e){var t=new s(o);return t.load(e),t}).then(function(e){var t=[i.Promise.resolve(e)],r=e.files;if(o.checkCRC32)for(var n=0;n<r.length;n++)t.push(f(r[n]));return i.Promise.all(t)}).then(function(e){for(var t=e.shift(),r=t.files,n=0;n<r.length;n++){var i=r[n],s=i.fileNameStr,a=u.resolve(i.fileNameStr);h.file(a,i.decompressed,{binary:!0,optimizedBinaryString:!0,date:i.date,dir:i.dir,comment:i.fileCommentStr.length?i.fileCommentStr:null,unixPermissions:i.unixPermissions,dosPermissions:i.dosPermissions,createFolders:o.createFolders}),i.dir||(h.file(a).unsafeOriginalName=s)}return t.zipComment.length&&(h.comment=t.zipComment),h})}},{"./external":6,"./nodejsUtils":14,"./stream/Crc32Probe":25,"./utf8":31,"./utils":32,"./zipEntries":33}],12:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../stream/GenericWorker");function s(e,t){i.call(this,"Nodejs stream input adapter for "+e),this._upstreamEnded=!1,this._bindStream(t)}n.inherits(s,i),s.prototype._bindStream=function(e){var t=this;(this._stream=e).pause(),e.on("data",function(e){t.push({data:e,meta:{percent:0}})}).on("error",function(e){t.isPaused?this.generatedError=e:t.error(e)}).on("end",function(){t.isPaused?t._upstreamEnded=!0:t.end()})},s.prototype.pause=function(){return!!i.prototype.pause.call(this)&&(this._stream.pause(),!0)},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(this._upstreamEnded?this.end():this._stream.resume(),!0)},t.exports=s},{"../stream/GenericWorker":28,"../utils":32}],13:[function(e,t,r){"use strict";var i=e("readable-stream").Readable;function n(e,t,r){i.call(this,t),this._helper=e;var n=this;e.on("data",function(e,t){n.push(e)||n._helper.pause(),r&&r(t)}).on("error",function(e){n.emit("error",e)}).on("end",function(){n.push(null)})}e("../utils").inherits(n,i),n.prototype._read=function(){this._helper.resume()},t.exports=n},{"../utils":32,"readable-stream":16}],14:[function(e,t,r){"use strict";t.exports={isNode:"undefined"!=typeof Buffer,newBufferFrom:function(e,t){if(Buffer.from&&Buffer.from!==Uint8Array.from)return Buffer.from(e,t);if("number"==typeof e)throw new Error('The "data" argument must not be a number');return new Buffer(e,t)},allocBuffer:function(e){if(Buffer.alloc)return Buffer.alloc(e);var t=new Buffer(e);return t.fill(0),t},isBuffer:function(e){return Buffer.isBuffer(e)},isStream:function(e){return e&&"function"==typeof e.on&&"function"==typeof e.pause&&"function"==typeof e.resume}}},{}],15:[function(e,t,r){"use strict";function s(e,t,r){var n,i=u.getTypeOf(t),s=u.extend(r||{},f);s.date=s.date||new Date,null!==s.compression&&(s.compression=s.compression.toUpperCase()),"string"==typeof s.unixPermissions&&(s.unixPermissions=parseInt(s.unixPermissions,8)),s.unixPermissions&&16384&s.unixPermissions&&(s.dir=!0),s.dosPermissions&&16&s.dosPermissions&&(s.dir=!0),s.dir&&(e=g(e)),s.createFolders&&(n=_(e))&&b.call(this,n,!0);var a="string"===i&&!1===s.binary&&!1===s.base64;r&&void 0!==r.binary||(s.binary=!a),(t instanceof c&&0===t.uncompressedSize||s.dir||!t||0===t.length)&&(s.base64=!1,s.binary=!0,t="",s.compression="STORE",i="string");var o=null;o=t instanceof c||t instanceof l?t:p.isNode&&p.isStream(t)?new m(e,t):u.prepareContent(e,t,s.binary,s.optimizedBinaryString,s.base64);var h=new d(e,o,s);this.files[e]=h}var i=e("./utf8"),u=e("./utils"),l=e("./stream/GenericWorker"),a=e("./stream/StreamHelper"),f=e("./defaults"),c=e("./compressedObject"),d=e("./zipObject"),o=e("./generate"),p=e("./nodejsUtils"),m=e("./nodejs/NodejsStreamInputAdapter"),_=function(e){"/"===e.slice(-1)&&(e=e.substring(0,e.length-1));var t=e.lastIndexOf("/");return 0<t?e.substring(0,t):""},g=function(e){return"/"!==e.slice(-1)&&(e+="/"),e},b=function(e,t){return t=void 0!==t?t:f.createFolders,e=g(e),this.files[e]||s.call(this,e,null,{dir:!0,createFolders:t}),this.files[e]};function h(e){return"[object RegExp]"===Object.prototype.toString.call(e)}var n={load:function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},forEach:function(e){var t,r,n;for(t in this.files)n=this.files[t],(r=t.slice(this.root.length,t.length))&&t.slice(0,this.root.length)===this.root&&e(r,n)},filter:function(r){var n=[];return this.forEach(function(e,t){r(e,t)&&n.push(t)}),n},file:function(e,t,r){if(1!==arguments.length)return e=this.root+e,s.call(this,e,t,r),this;if(h(e)){var n=e;return this.filter(function(e,t){return!t.dir&&n.test(e)})}var i=this.files[this.root+e];return i&&!i.dir?i:null},folder:function(r){if(!r)return this;if(h(r))return this.filter(function(e,t){return t.dir&&r.test(e)});var e=this.root+r,t=b.call(this,e),n=this.clone();return n.root=t.name,n},remove:function(r){r=this.root+r;var e=this.files[r];if(e||("/"!==r.slice(-1)&&(r+="/"),e=this.files[r]),e&&!e.dir)delete this.files[r];else for(var t=this.filter(function(e,t){return t.name.slice(0,r.length)===r}),n=0;n<t.length;n++)delete this.files[t[n].name];return this},generate:function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},generateInternalStream:function(e){var t,r={};try{if((r=u.extend(e||{},{streamFiles:!1,compression:"STORE",compressionOptions:null,type:"",platform:"DOS",comment:null,mimeType:"application/zip",encodeFileName:i.utf8encode})).type=r.type.toLowerCase(),r.compression=r.compression.toUpperCase(),"binarystring"===r.type&&(r.type="string"),!r.type)throw new Error("No output type specified.");u.checkSupport(r.type),"darwin"!==r.platform&&"freebsd"!==r.platform&&"linux"!==r.platform&&"sunos"!==r.platform||(r.platform="UNIX"),"win32"===r.platform&&(r.platform="DOS");var n=r.comment||this.comment||"";t=o.generateWorker(this,r,n)}catch(e){(t=new l("error")).error(e)}return new a(t,r.type||"string",r.mimeType)},generateAsync:function(e,t){return this.generateInternalStream(e).accumulate(t)},generateNodeStream:function(e,t){return(e=e||{}).type||(e.type="nodebuffer"),this.generateInternalStream(e).toNodejsStream(t)}};t.exports=n},{"./compressedObject":2,"./defaults":5,"./generate":9,"./nodejs/NodejsStreamInputAdapter":12,"./nodejsUtils":14,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31,"./utils":32,"./zipObject":35}],16:[function(e,t,r){"use strict";t.exports=e("stream")},{stream:void 0}],17:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e);for(var t=0;t<this.data.length;t++)e[t]=255&e[t]}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data[this.zero+e]},i.prototype.lastIndexOfSignature=function(e){for(var t=e.charCodeAt(0),r=e.charCodeAt(1),n=e.charCodeAt(2),i=e.charCodeAt(3),s=this.length-4;0<=s;--s)if(this.data[s]===t&&this.data[s+1]===r&&this.data[s+2]===n&&this.data[s+3]===i)return s-this.zero;return-1},i.prototype.readAndCheckSignature=function(e){var t=e.charCodeAt(0),r=e.charCodeAt(1),n=e.charCodeAt(2),i=e.charCodeAt(3),s=this.readData(4);return t===s[0]&&r===s[1]&&n===s[2]&&i===s[3]},i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return[];var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],18:[function(e,t,r){"use strict";var n=e("../utils");function i(e){this.data=e,this.length=e.length,this.index=0,this.zero=0}i.prototype={checkOffset:function(e){this.checkIndex(this.index+e)},checkIndex:function(e){if(this.length<this.zero+e||e<0)throw new Error("End of data reached (data length = "+this.length+", asked index = "+e+"). Corrupted zip ?")},setIndex:function(e){this.checkIndex(e),this.index=e},skip:function(e){this.setIndex(this.index+e)},byteAt:function(){},readInt:function(e){var t,r=0;for(this.checkOffset(e),t=this.index+e-1;t>=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i},{"../utils":32}],19:[function(e,t,r){"use strict";var n=e("./Uint8ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r<this._listeners[e].length;r++)this._listeners[e][r].call(this,t)},pipe:function(e){return e.registerPrevious(this)},registerPrevious:function(e){if(this.isLocked)throw new Error("The stream '"+this+"' has already been used.");this.streamInfo=e.streamInfo,this.mergeStreamInfo(),this.previous=e;var t=this;return e.on("data",function(e){t.processChunk(e)}),e.on("end",function(){t.end()}),e.on("error",function(e){t.error(e)}),this},pause:function(){return!this.isPaused&&!this.isFinished&&(this.isPaused=!0,this.previous&&this.previous.pause(),!0)},resume:function(){if(!this.isPaused||this.isFinished)return!1;var e=this.isPaused=!1;return this.generatedError&&(this.error(this.generatedError),e=!0),this.previous&&this.previous.resume(),!e},flush:function(){},processChunk:function(e){this.push(e)},withStreamInfo:function(e,t){return this.extraStreamInfo[e]=t,this.mergeStreamInfo(),this},mergeStreamInfo:function(){for(var e in this.extraStreamInfo)Object.prototype.hasOwnProperty.call(this.extraStreamInfo,e)&&(this.streamInfo[e]=this.extraStreamInfo[e])},lock:function(){if(this.isLocked)throw new Error("The stream '"+this+"' has already been used.");this.isLocked=!0,this.previous&&this.previous.lock()},toString:function(){var e="Worker "+this.name;return this.previous?this.previous+" -> "+e:e}},t.exports=n},{}],29:[function(e,t,r){"use strict";var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t)}).on("error",function(e){n=[],r(e)}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r<t.length;r++)s+=t[r].length;switch(e){case"string":return t.join("");case"array":return Array.prototype.concat.apply([],t);case"uint8array":for(i=new Uint8Array(s),r=0;r<t.length;r++)i.set(t[r],n),n+=t[r].length;return i;case"nodebuffer":return Buffer.concat(t);default:throw new Error("concat : unsupported type '"+e+"'")}}(i,n),a);t(e)}catch(e){r(e)}n=[]}).resume()})}function f(e,t,r){var n=t;switch(t){case"blob":case"arraybuffer":n="uint8array";break;case"base64":n="string"}try{this._internalType=n,this._outputType=t,this._mimeType=r,h.checkSupport(n),this._worker=e.pipe(new i(n)),e.lock()}catch(e){this._worker=new s("error"),this._worker.error(e)}}f.prototype={accumulate:function(e){return l(this,e)},on:function(e,t){var r=this;return"data"===e?this._worker.on(e,function(e){t.call(r,e.data,e.meta)}):this._worker.on(e,function(){h.delay(t,arguments,r)}),this},resume:function(){return h.delay(this._worker.resume,[],this._worker),this},pause:function(){return this._worker.pause(),this},toNodejsStream:function(e){if(h.checkSupport("nodestream"),"nodebuffer"!==this._outputType)throw new Error(this._outputType+" is not supported by this method");return new o(this,{objectMode:"nodebuffer"!==this._outputType},e)}},t.exports=f},{"../base64":1,"../external":6,"../nodejs/NodejsStreamOutputAdapter":13,"../support":30,"../utils":32,"./ConvertWorker":24,"./GenericWorker":28}],30:[function(e,t,r){"use strict";if(r.base64=!0,r.array=!0,r.string=!0,r.arraybuffer="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof Uint8Array,r.nodebuffer="undefined"!=typeof Buffer,r.uint8array="undefined"!=typeof Uint8Array,"undefined"==typeof ArrayBuffer)r.blob=!1;else{var n=new ArrayBuffer(0);try{r.blob=0===new Blob([n],{type:"application/zip"}).size}catch(e){try{var i=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);i.append(n),r.blob=0===i.getBlob("application/zip").size}catch(e){r.blob=!1}}}try{r.nodestream=!!e("readable-stream").Readable}catch(e){r.nodestream=!1}},{"readable-stream":16}],31:[function(e,t,s){"use strict";for(var o=e("./utils"),h=e("./support"),r=e("./nodejsUtils"),n=e("./stream/GenericWorker"),u=new Array(256),i=0;i<256;i++)u[i]=252<=i?6:248<=i?5:240<=i?4:224<=i?3:192<=i?2:1;u[254]=u[254]=1;function a(){n.call(this,"utf-8 decode"),this.leftOver=null}function l(){n.call(this,"utf-8 encode")}s.utf8encode=function(e){return h.nodebuffer?r.newBufferFrom(e,"utf-8"):function(e){var t,r,n,i,s,a=e.length,o=0;for(i=0;i<a;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),o+=r<128?1:r<2048?2:r<65536?3:4;for(t=h.uint8array?new Uint8Array(o):new Array(o),i=s=0;s<o;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),r<128?t[s++]=r:(r<2048?t[s++]=192|r>>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t<s;)if((n=e[t++])<128)a[r++]=n;else if(4<(i=u[n]))a[r++]=65533,t+=i-1;else{for(n&=2===i?31:3===i?15:7;1<i&&t<s;)n=n<<6|63&e[t++],i--;1<i?a[r++]=65533:n<65536?a[r++]=n:(n-=65536,a[r++]=55296|n>>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r<e.length;++r)t[r]=255&e.charCodeAt(r);return t}e("setimmediate"),a.newBlob=function(t,r){a.checkSupport("blob");try{return new Blob([t],{type:r})}catch(e){try{var n=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);return n.append(t),n.getBlob(r)}catch(e){throw new Error("Bug : can't construct the Blob.")}}};var i={stringifyByChunk:function(e,t,r){var n=[],i=0,s=e.length;if(s<=r)return String.fromCharCode.apply(null,e);for(;i<s;)"array"===t||"nodebuffer"===t?n.push(String.fromCharCode.apply(null,e.slice(i,Math.min(i+r,s)))):n.push(String.fromCharCode.apply(null,e.subarray(i,Math.min(i+r,s)))),i+=r;return n.join("")},stringifyByChar:function(e){for(var t="",r=0;r<e.length;r++)t+=String.fromCharCode(e[r]);return t},applyCanBeUsed:{uint8array:function(){try{return o.uint8array&&1===String.fromCharCode.apply(null,new Uint8Array(1)).length}catch(e){return!1}}(),nodebuffer:function(){try{return o.nodebuffer&&1===String.fromCharCode.apply(null,r.allocBuffer(1)).length}catch(e){return!1}}()}};function s(e){var t=65536,r=a.getTypeOf(e),n=!0;if("uint8array"===r?n=i.applyCanBeUsed.uint8array:"nodebuffer"===r&&(n=i.applyCanBeUsed.nodebuffer),n)for(;1<t;)try{return i.stringifyByChunk(e,r,t)}catch(e){t=Math.floor(t/2)}return i.stringifyByChar(e)}function f(e,t){for(var r=0;r<e.length;r++)t[r]=e[r];return t}a.applyFromCharCode=s;var c={};c.string={string:n,array:function(e){return l(e,new Array(e.length))},arraybuffer:function(e){return c.string.uint8array(e).buffer},uint8array:function(e){return l(e,new Uint8Array(e.length))},nodebuffer:function(e){return l(e,r.allocBuffer(e.length))}},c.array={string:s,array:n,arraybuffer:function(e){return new Uint8Array(e).buffer},uint8array:function(e){return new Uint8Array(e)},nodebuffer:function(e){return r.newBufferFrom(e)}},c.arraybuffer={string:function(e){return s(new Uint8Array(e))},array:function(e){return f(new Uint8Array(e),new Array(e.byteLength))},arraybuffer:n,uint8array:function(e){return new Uint8Array(e)},nodebuffer:function(e){return r.newBufferFrom(new Uint8Array(e))}},c.uint8array={string:s,array:function(e){return f(e,new Array(e.length))},arraybuffer:function(e){return e.buffer},uint8array:n,nodebuffer:function(e){return r.newBufferFrom(e)}},c.nodebuffer={string:s,array:function(e){return f(e,new Array(e.length))},arraybuffer:function(e){return c.nodebuffer.uint8array(e).buffer},uint8array:function(e){return f(e,new Uint8Array(e.length))},nodebuffer:n},a.transformTo=function(e,t){if(t=t||"",!e)return t;a.checkSupport(e);var r=a.getTypeOf(t);return c[r][e](t)},a.resolve=function(e){for(var t=e.split("/"),r=[],n=0;n<t.length;n++){var i=t[n];"."===i||""===i&&0!==n&&n!==t.length-1||(".."===i?r.pop():r.push(i))}return r.join("/")},a.getTypeOf=function(e){return"string"==typeof e?"string":"[object Array]"===Object.prototype.toString.call(e)?"array":o.nodebuffer&&r.isBuffer(e)?"nodebuffer":o.uint8array&&e instanceof Uint8Array?"uint8array":o.arraybuffer&&e instanceof ArrayBuffer?"arraybuffer":void 0},a.checkSupport=function(e){if(!o[e.toLowerCase()])throw new Error(e+" is not supported by this platform")},a.MAX_VALUE_16BITS=65535,a.MAX_VALUE_32BITS=-1,a.pretty=function(e){var t,r,n="";for(r=0;r<(e||"").length;r++)n+="\\x"+((t=e.charCodeAt(r))<16?"0":"")+t.toString(16).toUpperCase();return n},a.delay=function(e,t,r){setImmediate(function(){e.apply(r||null,t||[])})},a.inherits=function(e,t){function r(){}r.prototype=t.prototype,e.prototype=new r},a.extend=function(){var e,t,r={};for(e=0;e<arguments.length;e++)for(t in arguments[e])Object.prototype.hasOwnProperty.call(arguments[e],t)&&void 0===r[t]&&(r[t]=arguments[e][t]);return r},a.prepareContent=function(r,e,n,i,s){return u.Promise.resolve(e).then(function(n){return o.blob&&(n instanceof Blob||-1!==["[object File]","[object Blob]"].indexOf(Object.prototype.toString.call(n)))&&"undefined"!=typeof FileReader?new u.Promise(function(t,r){var e=new FileReader;e.onload=function(e){t(e.target.result)},e.onerror=function(e){r(e.target.error)},e.readAsArrayBuffer(n)}):n}).then(function(e){var t=a.getTypeOf(e);return t?("arraybuffer"===t?e=a.transformTo("uint8array",e):"string"===t&&(s?e=h.decode(e):n&&!0!==i&&(e=function(e){return l(e,o.uint8array?new Uint8Array(e.length):new Array(e.length))}(e))),e):u.Promise.reject(new Error("Can't read the data of '"+r+"'. Is it in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?"))})}},{"./base64":1,"./external":6,"./nodejsUtils":14,"./support":30,setimmediate:54}],33:[function(e,t,r){"use strict";var n=e("./reader/readerFor"),i=e("./utils"),s=e("./signature"),a=e("./zipEntry"),o=e("./support");function h(e){this.files=[],this.loadOptions=e}h.prototype={checkSignature:function(e){if(!this.reader.readAndCheckSignature(e)){this.reader.index-=4;var t=this.reader.readString(4);throw new Error("Corrupted zip or bug: unexpected signature ("+i.pretty(t)+", expected "+i.pretty(e)+")")}},isSignature:function(e,t){var r=this.reader.index;this.reader.setIndex(e);var n=this.reader.readString(4)===t;return this.reader.setIndex(r),n},readBlockEndOfCentral:function(){this.diskNumber=this.reader.readInt(2),this.diskWithCentralDirStart=this.reader.readInt(2),this.centralDirRecordsOnThisDisk=this.reader.readInt(2),this.centralDirRecords=this.reader.readInt(2),this.centralDirSize=this.reader.readInt(4),this.centralDirOffset=this.reader.readInt(4),this.zipCommentLength=this.reader.readInt(2);var e=this.reader.readData(this.zipCommentLength),t=o.uint8array?"uint8array":"array",r=i.transformTo(t,e);this.zipComment=this.loadOptions.decodeFileName(r)},readBlockZip64EndOfCentral:function(){this.zip64EndOfCentralSize=this.reader.readInt(8),this.reader.skip(4),this.diskNumber=this.reader.readInt(4),this.diskWithCentralDirStart=this.reader.readInt(4),this.centralDirRecordsOnThisDisk=this.reader.readInt(8),this.centralDirRecords=this.reader.readInt(8),this.centralDirSize=this.reader.readInt(8),this.centralDirOffset=this.reader.readInt(8),this.zip64ExtensibleData={};for(var e,t,r,n=this.zip64EndOfCentralSize-44;0<n;)e=this.reader.readInt(2),t=this.reader.readInt(4),r=this.reader.readData(t),this.zip64ExtensibleData[e]={id:e,length:t,value:r}},readBlockZip64EndOfCentralLocator:function(){if(this.diskWithZip64CentralDirStart=this.reader.readInt(4),this.relativeOffsetEndOfZip64CentralDir=this.reader.readInt(8),this.disksCount=this.reader.readInt(4),1<this.disksCount)throw new Error("Multi-volumes zip are not supported")},readLocalFiles:function(){var e,t;for(e=0;e<this.files.length;e++)t=this.files[e],this.reader.setIndex(t.localHeaderOffset),this.checkSignature(s.LOCAL_FILE_HEADER),t.readLocalPart(this.reader),t.handleUTF8(),t.processAttributes()},readCentralDir:function(){var e;for(this.reader.setIndex(this.centralDirOffset);this.reader.readAndCheckSignature(s.CENTRAL_FILE_HEADER);)(e=new a({zip64:this.zip64},this.loadOptions)).readCentralPart(this.reader),this.files.push(e);if(this.centralDirRecords!==this.files.length&&0!==this.centralDirRecords&&0===this.files.length)throw new Error("Corrupted zip or bug: expected "+this.centralDirRecords+" records in central dir, got "+this.files.length)},readEndOfCentral:function(){var e=this.reader.lastIndexOfSignature(s.CENTRAL_DIRECTORY_END);if(e<0)throw!this.isSignature(0,s.LOCAL_FILE_HEADER)?new Error("Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html"):new Error("Corrupted zip: can't find end of central directory");this.reader.setIndex(e);var t=e;if(this.checkSignature(s.CENTRAL_DIRECTORY_END),this.readBlockEndOfCentral(),this.diskNumber===i.MAX_VALUE_16BITS||this.diskWithCentralDirStart===i.MAX_VALUE_16BITS||this.centralDirRecordsOnThisDisk===i.MAX_VALUE_16BITS||this.centralDirRecords===i.MAX_VALUE_16BITS||this.centralDirSize===i.MAX_VALUE_32BITS||this.centralDirOffset===i.MAX_VALUE_32BITS){if(this.zip64=!0,(e=this.reader.lastIndexOfSignature(s.ZIP64_CENTRAL_DIRECTORY_LOCATOR))<0)throw new Error("Corrupted zip: can't find the ZIP64 end of central directory locator");if(this.reader.setIndex(e),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_LOCATOR),this.readBlockZip64EndOfCentralLocator(),!this.isSignature(this.relativeOffsetEndOfZip64CentralDir,s.ZIP64_CENTRAL_DIRECTORY_END)&&(this.relativeOffsetEndOfZip64CentralDir=this.reader.lastIndexOfSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.relativeOffsetEndOfZip64CentralDir<0))throw new Error("Corrupted zip: can't find the ZIP64 end of central directory");this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir),this.checkSignature(s.ZIP64_CENTRAL_DIRECTORY_END),this.readBlockZip64EndOfCentral()}var r=this.centralDirOffset+this.centralDirSize;this.zip64&&(r+=20,r+=12+this.zip64EndOfCentralSize);var n=t-r;if(0<n)this.isSignature(t,s.CENTRAL_FILE_HEADER)||(this.reader.zero=n);else if(n<0)throw new Error("Corrupted zip: missing "+Math.abs(n)+" bytes.")},prepareReader:function(e){this.reader=n(e)},load:function(e){this.prepareReader(e),this.readEndOfCentral(),this.readCentralDir(),this.readLocalFiles()}},t.exports=h},{"./reader/readerFor":22,"./signature":23,"./support":30,"./utils":32,"./zipEntry":34}],34:[function(e,t,r){"use strict";var n=e("./reader/readerFor"),s=e("./utils"),i=e("./compressedObject"),a=e("./crc32"),o=e("./utf8"),h=e("./compressions"),u=e("./support");function l(e,t){this.options=e,this.loadOptions=t}l.prototype={isEncrypted:function(){return 1==(1&this.bitFlag)},useUTF8:function(){return 2048==(2048&this.bitFlag)},readLocalPart:function(e){var t,r;if(e.skip(22),this.fileNameLength=e.readInt(2),r=e.readInt(2),this.fileName=e.readData(this.fileNameLength),e.skip(r),-1===this.compressedSize||-1===this.uncompressedSize)throw new Error("Bug or corrupted zip : didn't get enough information from the central directory (compressedSize === -1 || uncompressedSize === -1)");if(null===(t=function(e){for(var t in h)if(Object.prototype.hasOwnProperty.call(h,t)&&h[t].magic===e)return h[t];return null}(this.compressionMethod)))throw new Error("Corrupted zip : compression "+s.pretty(this.compressionMethod)+" unknown (inner file : "+s.transformTo("string",this.fileName)+")");this.decompressed=new i(this.compressedSize,this.uncompressedSize,this.crc32,t,e.readData(this.compressedSize))},readCentralPart:function(e){this.versionMadeBy=e.readInt(2),e.skip(2),this.bitFlag=e.readInt(2),this.compressionMethod=e.readString(2),this.date=e.readDate(),this.crc32=e.readInt(4),this.compressedSize=e.readInt(4),this.uncompressedSize=e.readInt(4);var t=e.readInt(2);if(this.extraFieldsLength=e.readInt(2),this.fileCommentLength=e.readInt(2),this.diskNumberStart=e.readInt(2),this.internalFileAttributes=e.readInt(2),this.externalFileAttributes=e.readInt(4),this.localHeaderOffset=e.readInt(4),this.isEncrypted())throw new Error("Encrypted zip are not supported");e.skip(t),this.readExtraFields(e),this.parseZIP64ExtraField(e),this.fileComment=e.readData(this.fileCommentLength)},processAttributes:function(){this.unixPermissions=null,this.dosPermissions=null;var e=this.versionMadeBy>>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4<i;)t=e.readInt(2),r=e.readInt(2),n=e.readData(r),this.extraFields[t]={id:t,length:r,value:n};e.setIndex(i)},handleUTF8:function(){var e=u.uint8array?"uint8array":"array";if(this.useUTF8())this.fileNameStr=o.utf8decode(this.fileName),this.fileCommentStr=o.utf8decode(this.fileComment);else{var t=this.findExtraFieldUnicodePath();if(null!==t)this.fileNameStr=t;else{var r=s.transformTo(e,this.fileName);this.fileNameStr=this.loadOptions.decodeFileName(r)}var n=this.findExtraFieldUnicodeComment();if(null!==n)this.fileCommentStr=n;else{var i=s.transformTo(e,this.fileComment);this.fileCommentStr=this.loadOptions.decodeFileName(i)}}},findExtraFieldUnicodePath:function(){var e=this.extraFields[28789];if(e){var t=n(e.value);return 1!==t.readInt(1)?null:a(this.fileName)!==t.readInt(4)?null:o.utf8decode(t.readData(e.length-5))}return null},findExtraFieldUnicodeComment:function(){var e=this.extraFields[25461];if(e){var t=n(e.value);return 1!==t.readInt(1)?null:a(this.fileComment)!==t.readInt(4)?null:o.utf8decode(t.readData(e.length-5))}return null}},t.exports=l},{"./compressedObject":2,"./compressions":3,"./crc32":4,"./reader/readerFor":22,"./support":30,"./utf8":31,"./utils":32}],35:[function(e,t,r){"use strict";function n(e,t,r){this.name=e,this.dir=r.dir,this.date=r.date,this.comment=r.comment,this.unixPermissions=r.unixPermissions,this.dosPermissions=r.dosPermissions,this._data=t,this._dataBinary=r.binary,this.options={compression:r.compression,compressionOptions:r.compressionOptions}}var s=e("./stream/StreamHelper"),i=e("./stream/DataWorker"),a=e("./utf8"),o=e("./compressedObject"),h=e("./stream/GenericWorker");n.prototype={internalStream:function(e){var t=null,r="string";try{if(!e)throw new Error("No output type specified.");var n="string"===(r=e.toLowerCase())||"text"===r;"binarystring"!==r&&"text"!==r||(r="string"),t=this._decompressWorker();var i=!this._dataBinary;i&&!n&&(t=t.pipe(new a.Utf8EncodeWorker)),!i&&n&&(t=t.pipe(new a.Utf8DecodeWorker))}catch(e){(t=new h("error")).error(e)}return new s(t,r,"")},async:function(e,t){return this.internalStream(e).accumulate(t)},nodeStream:function(e,t){return this.internalStream(e||"nodebuffer").toNodejsStream(t)},_compressWorker:function(e,t){if(this._data instanceof o&&this._data.compression.magic===e.magic)return this._data.getCompressedWorker();var r=this._decompressWorker();return this._dataBinary||(r=r.pipe(new a.Utf8EncodeWorker)),o.createWorkerFrom(r,e,t)},_decompressWorker:function(){return this._data instanceof o?this._data.getContentWorker():this._data instanceof h?this._data:new i(this._data)}};for(var u=["asText","asBinary","asNodeBuffer","asUint8Array","asArrayBuffer"],l=function(){throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide.")},f=0;f<u.length;f++)n.prototype[u[f]]=l;t.exports=n},{"./compressedObject":2,"./stream/DataWorker":27,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31}],36:[function(e,l,t){(function(t){"use strict";var r,n,e=t.MutationObserver||t.WebKitMutationObserver;if(e){var i=0,s=new e(u),a=t.document.createTextNode("");s.observe(a,{characterData:!0}),r=function(){a.data=i=++i%2}}else if(t.setImmediate||void 0===t.MessageChannel)r="document"in t&&"onreadystatechange"in t.document.createElement("script")?function(){var e=t.document.createElement("script");e.onreadystatechange=function(){u(),e.onreadystatechange=null,e.parentNode.removeChild(e),e=null},t.document.documentElement.appendChild(e)}:function(){setTimeout(u,0)};else{var o=new t.MessageChannel;o.port1.onmessage=u,r=function(){o.port2.postMessage(0)}}var h=[];function u(){var e,t;n=!0;for(var r=h.length;r;){for(t=h,h=[],e=-1;++e<r;)t[e]();r=h.length}n=!1}l.exports=function(e){1!==h.push(e)||n||r()}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],37:[function(e,t,r){"use strict";var i=e("immediate");function u(){}var l={},s=["REJECTED"],a=["FULFILLED"],n=["PENDING"];function o(e){if("function"!=typeof e)throw new TypeError("resolver must be a function");this.state=n,this.queue=[],this.outcome=void 0,e!==u&&d(this,e)}function h(e,t,r){this.promise=e,"function"==typeof t&&(this.onFulfilled=t,this.callFulfilled=this.otherCallFulfilled),"function"==typeof r&&(this.onRejected=r,this.callRejected=this.otherCallRejected)}function f(t,r,n){i(function(){var e;try{e=r(n)}catch(e){return l.reject(t,e)}e===t?l.reject(t,new TypeError("Cannot resolve promise with itself")):l.resolve(t,e)})}function c(e){var t=e&&e.then;if(e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof t)return function(){t.apply(e,arguments)}}function d(t,e){var r=!1;function n(e){r||(r=!0,l.reject(t,e))}function i(e){r||(r=!0,l.resolve(t,e))}var s=p(function(){e(i,n)});"error"===s.status&&n(s.value)}function p(e,t){var r={};try{r.value=e(t),r.status="success"}catch(e){r.status="error",r.value=e}return r}(t.exports=o).prototype.finally=function(t){if("function"!=typeof t)return this;var r=this.constructor;return this.then(function(e){return r.resolve(t()).then(function(){return e})},function(e){return r.resolve(t()).then(function(){throw e})})},o.prototype.catch=function(e){return this.then(null,e)},o.prototype.then=function(e,t){if("function"!=typeof e&&this.state===a||"function"!=typeof t&&this.state===s)return this;var r=new this.constructor(u);this.state!==n?f(r,this.state===a?e:t,this.outcome):this.queue.push(new h(r,e,t));return r},h.prototype.callFulfilled=function(e){l.resolve(this.promise,e)},h.prototype.otherCallFulfilled=function(e){f(this.promise,this.onFulfilled,e)},h.prototype.callRejected=function(e){l.reject(this.promise,e)},h.prototype.otherCallRejected=function(e){f(this.promise,this.onRejected,e)},l.resolve=function(e,t){var r=p(c,t);if("error"===r.status)return l.reject(e,r.value);var n=r.value;if(n)d(e,n);else{e.state=a,e.outcome=t;for(var i=-1,s=e.queue.length;++i<s;)e.queue[i].callFulfilled(t)}return e},l.reject=function(e,t){e.state=s,e.outcome=t;for(var r=-1,n=e.queue.length;++r<n;)e.queue[r].callRejected(t);return e},o.resolve=function(e){if(e instanceof this)return e;return l.resolve(new this(u),e)},o.reject=function(e){var t=new this(u);return l.reject(t,e)},o.all=function(e){var r=this;if("[object Array]"!==Object.prototype.toString.call(e))return this.reject(new TypeError("must be an array"));var n=e.length,i=!1;if(!n)return this.resolve([]);var s=new Array(n),a=0,t=-1,o=new this(u);for(;++t<n;)h(e[t],t);return o;function h(e,t){r.resolve(e).then(function(e){s[t]=e,++a!==n||i||(i=!0,l.resolve(o,s))},function(e){i||(i=!0,l.reject(o,e))})}},o.race=function(e){var t=this;if("[object Array]"!==Object.prototype.toString.call(e))return this.reject(new TypeError("must be an array"));var r=e.length,n=!1;if(!r)return this.resolve([]);var i=-1,s=new this(u);for(;++i<r;)a=e[i],t.resolve(a).then(function(e){n||(n=!0,l.resolve(s,e))},function(e){n||(n=!0,l.reject(s,e))});var a;return s}},{immediate:36}],38:[function(e,t,r){"use strict";var n={};(0,e("./lib/utils/common").assign)(n,e("./lib/deflate"),e("./lib/inflate"),e("./lib/zlib/constants")),t.exports=n},{"./lib/deflate":39,"./lib/inflate":40,"./lib/utils/common":41,"./lib/zlib/constants":44}],39:[function(e,t,r){"use strict";var a=e("./zlib/deflate"),o=e("./utils/common"),h=e("./utils/strings"),i=e("./zlib/messages"),s=e("./zlib/zstream"),u=Object.prototype.toString,l=0,f=-1,c=0,d=8;function p(e){if(!(this instanceof p))return new p(e);this.options=o.assign({level:f,method:d,chunkSize:16384,windowBits:15,memLevel:8,strategy:c,to:""},e||{});var t=this.options;t.raw&&0<t.windowBits?t.windowBits=-t.windowBits:t.gzip&&0<t.windowBits&&t.windowBits<16&&(t.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new s,this.strm.avail_out=0;var r=a.deflateInit2(this.strm,t.level,t.method,t.windowBits,t.memLevel,t.strategy);if(r!==l)throw new Error(i[r]);if(t.header&&a.deflateSetHeader(this.strm,t.header),t.dictionary){var n;if(n="string"==typeof t.dictionary?h.string2buf(t.dictionary):"[object ArrayBuffer]"===u.call(t.dictionary)?new Uint8Array(t.dictionary):t.dictionary,(r=a.deflateSetDictionary(this.strm,n))!==l)throw new Error(i[r]);this._dict_set=!0}}function n(e,t){var r=new p(t);if(r.push(e,!0),r.err)throw r.msg||i[r.err];return r.result}p.prototype.push=function(e,t){var r,n,i=this.strm,s=this.options.chunkSize;if(this.ended)return!1;n=t===~~t?t:!0===t?4:0,"string"==typeof e?i.input=h.string2buf(e):"[object ArrayBuffer]"===u.call(e)?i.input=new Uint8Array(e):i.input=e,i.next_in=0,i.avail_in=i.input.length;do{if(0===i.avail_out&&(i.output=new o.Buf8(s),i.next_out=0,i.avail_out=s),1!==(r=a.deflate(i,n))&&r!==l)return this.onEnd(r),!(this.ended=!0);0!==i.avail_out&&(0!==i.avail_in||4!==n&&2!==n)||("string"===this.options.to?this.onData(h.buf2binstring(o.shrinkBuf(i.output,i.next_out))):this.onData(o.shrinkBuf(i.output,i.next_out)))}while((0<i.avail_in||0===i.avail_out)&&1!==r);return 4===n?(r=a.deflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===l):2!==n||(this.onEnd(l),!(i.avail_out=0))},p.prototype.onData=function(e){this.chunks.push(e)},p.prototype.onEnd=function(e){e===l&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=o.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},r.Deflate=p,r.deflate=n,r.deflateRaw=function(e,t){return(t=t||{}).raw=!0,n(e,t)},r.gzip=function(e,t){return(t=t||{}).gzip=!0,n(e,t)}},{"./utils/common":41,"./utils/strings":42,"./zlib/deflate":46,"./zlib/messages":51,"./zlib/zstream":53}],40:[function(e,t,r){"use strict";var c=e("./zlib/inflate"),d=e("./utils/common"),p=e("./utils/strings"),m=e("./zlib/constants"),n=e("./zlib/messages"),i=e("./zlib/zstream"),s=e("./zlib/gzheader"),_=Object.prototype.toString;function a(e){if(!(this instanceof a))return new a(e);this.options=d.assign({chunkSize:16384,windowBits:0,to:""},e||{});var t=this.options;t.raw&&0<=t.windowBits&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(0<=t.windowBits&&t.windowBits<16)||e&&e.windowBits||(t.windowBits+=32),15<t.windowBits&&t.windowBits<48&&0==(15&t.windowBits)&&(t.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new i,this.strm.avail_out=0;var r=c.inflateInit2(this.strm,t.windowBits);if(r!==m.Z_OK)throw new Error(n[r]);this.header=new s,c.inflateGetHeader(this.strm,this.header)}function o(e,t){var r=new a(t);if(r.push(e,!0),r.err)throw r.msg||n[r.err];return r.result}a.prototype.push=function(e,t){var r,n,i,s,a,o,h=this.strm,u=this.options.chunkSize,l=this.options.dictionary,f=!1;if(this.ended)return!1;n=t===~~t?t:!0===t?m.Z_FINISH:m.Z_NO_FLUSH,"string"==typeof e?h.input=p.binstring2buf(e):"[object ArrayBuffer]"===_.call(e)?h.input=new Uint8Array(e):h.input=e,h.next_in=0,h.avail_in=h.input.length;do{if(0===h.avail_out&&(h.output=new d.Buf8(u),h.next_out=0,h.avail_out=u),(r=c.inflate(h,m.Z_NO_FLUSH))===m.Z_NEED_DICT&&l&&(o="string"==typeof l?p.string2buf(l):"[object ArrayBuffer]"===_.call(l)?new Uint8Array(l):l,r=c.inflateSetDictionary(this.strm,o)),r===m.Z_BUF_ERROR&&!0===f&&(r=m.Z_OK,f=!1),r!==m.Z_STREAM_END&&r!==m.Z_OK)return this.onEnd(r),!(this.ended=!0);h.next_out&&(0!==h.avail_out&&r!==m.Z_STREAM_END&&(0!==h.avail_in||n!==m.Z_FINISH&&n!==m.Z_SYNC_FLUSH)||("string"===this.options.to?(i=p.utf8border(h.output,h.next_out),s=h.next_out-i,a=p.buf2string(h.output,i),h.next_out=s,h.avail_out=u-s,s&&d.arraySet(h.output,h.output,i,s,0),this.onData(a)):this.onData(d.shrinkBuf(h.output,h.next_out)))),0===h.avail_in&&0===h.avail_out&&(f=!0)}while((0<h.avail_in||0===h.avail_out)&&r!==m.Z_STREAM_END);return r===m.Z_STREAM_END&&(n=m.Z_FINISH),n===m.Z_FINISH?(r=c.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===m.Z_OK):n!==m.Z_SYNC_FLUSH||(this.onEnd(m.Z_OK),!(h.avail_out=0))},a.prototype.onData=function(e){this.chunks.push(e)},a.prototype.onEnd=function(e){e===m.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=d.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg},r.Inflate=a,r.inflate=o,r.inflateRaw=function(e,t){return(t=t||{}).raw=!0,o(e,t)},r.ungzip=o},{"./utils/common":41,"./utils/strings":42,"./zlib/constants":44,"./zlib/gzheader":47,"./zlib/inflate":49,"./zlib/messages":51,"./zlib/zstream":53}],41:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;r.assign=function(e){for(var t=Array.prototype.slice.call(arguments,1);t.length;){var r=t.shift();if(r){if("object"!=typeof r)throw new TypeError(r+"must be non-object");for(var n in r)r.hasOwnProperty(n)&&(e[n]=r[n])}}return e},r.shrinkBuf=function(e,t){return e.length===t?e:e.subarray?e.subarray(0,t):(e.length=t,e)};var i={arraySet:function(e,t,r,n,i){if(t.subarray&&e.subarray)e.set(t.subarray(r,r+n),i);else for(var s=0;s<n;s++)e[i+s]=t[r+s]},flattenChunks:function(e){var t,r,n,i,s,a;for(t=n=0,r=e.length;t<r;t++)n+=e[t].length;for(a=new Uint8Array(n),t=i=0,r=e.length;t<r;t++)s=e[t],a.set(s,i),i+=s.length;return a}},s={arraySet:function(e,t,r,n,i){for(var s=0;s<n;s++)e[i+s]=t[r+s]},flattenChunks:function(e){return[].concat.apply([],e)}};r.setTyped=function(e){e?(r.Buf8=Uint8Array,r.Buf16=Uint16Array,r.Buf32=Int32Array,r.assign(r,i)):(r.Buf8=Array,r.Buf16=Array,r.Buf32=Array,r.assign(r,s))},r.setTyped(n)},{}],42:[function(e,t,r){"use strict";var h=e("./common"),i=!0,s=!0;try{String.fromCharCode.apply(null,[0])}catch(e){i=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(e){s=!1}for(var u=new h.Buf8(256),n=0;n<256;n++)u[n]=252<=n?6:248<=n?5:240<=n?4:224<=n?3:192<=n?2:1;function l(e,t){if(t<65537&&(e.subarray&&s||!e.subarray&&i))return String.fromCharCode.apply(null,h.shrinkBuf(e,t));for(var r="",n=0;n<t;n++)r+=String.fromCharCode(e[n]);return r}u[254]=u[254]=1,r.string2buf=function(e){var t,r,n,i,s,a=e.length,o=0;for(i=0;i<a;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),o+=r<128?1:r<2048?2:r<65536?3:4;for(t=new h.Buf8(o),i=s=0;s<o;i++)55296==(64512&(r=e.charCodeAt(i)))&&i+1<a&&56320==(64512&(n=e.charCodeAt(i+1)))&&(r=65536+(r-55296<<10)+(n-56320),i++),r<128?t[s++]=r:(r<2048?t[s++]=192|r>>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r<n;r++)t[r]=e.charCodeAt(r);return t},r.buf2string=function(e,t){var r,n,i,s,a=t||e.length,o=new Array(2*a);for(r=n=0;r<a;)if((i=e[r++])<128)o[n++]=i;else if(4<(s=u[i]))o[n++]=65533,r+=s-1;else{for(i&=2===s?31:3===s?15:7;1<s&&r<a;)i=i<<6|63&e[r++],s--;1<s?o[n++]=65533:i<65536?o[n++]=i:(i-=65536,o[n++]=55296|i>>10&1023,o[n++]=56320|1023&i)}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}},{"./common":41}],43:[function(e,t,r){"use strict";t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3<r?2e3:r;s=s+(i=i+t[n++]|0)|0,--a;);i%=65521,s%=65521}return i|s<<16|0}},{}],44:[function(e,t,r){"use strict";t.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],45:[function(e,t,r){"use strict";var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}},{}],46:[function(e,t,r){"use strict";var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return(e<<1)-(4<e?9:0)}function D(e){for(var t=e.length;0<=--t;)e[t]=0}function F(e){var t=e.state,r=t.pending;r>e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&s<c);if(n=S-(c-s),s=c-S,a<n){if(e.match_start=t,o<=(a=n))break;d=u[s+a-1],p=u[s+a]}}}while((t=f[t&l])>h&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u<l&&(l=u),r=0===l?0:(a.avail_in-=l,c.arraySet(o,a.input,a.next_in,l,h),1===a.state.wrap?a.adler=d(a.adler,o,l,h):2===a.state.wrap&&(a.adler=p(a.adler,o,l,h)),a.next_in+=l,a.total_in+=l,l),e.lookahead+=r,e.lookahead+e.insert>=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<<e.hash_shift^e.window[s+1])&e.hash_mask;e.insert&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[s+x-1])&e.hash_mask,e.prev[s&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=s,s++,e.insert--,!(e.lookahead+e.insert<x)););}while(e.lookahead<z&&0!==e.strm.avail_in)}function Z(e,t){for(var r,n;;){if(e.lookahead<z){if(j(e),e.lookahead<z&&t===l)return A;if(0===e.lookahead)break}if(r=0,e.lookahead>=x&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!==r&&e.strstart-r<=e.w_size-z&&(e.match_length=L(e,r)),e.match_length>=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart,0!=--e.match_length;);e.strstart++}else e.strstart+=e.match_length,e.match_length=0,e.ins_h=e.window[e.strstart],e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+1])&e.hash_mask;else n=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++;if(n&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=e.strstart<x-1?e.strstart:x-1,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}function W(e,t){for(var r,n,i;;){if(e.lookahead<z){if(j(e),e.lookahead<z&&t===l)return A;if(0===e.lookahead)break}if(r=0,e.lookahead>=x&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),e.prev_length=e.match_length,e.prev_match=e.match_start,e.match_length=x-1,0!==r&&e.prev_length<e.max_lazy_match&&e.strstart-r<=e.w_size-z&&(e.match_length=L(e,r),e.match_length<=5&&(1===e.strategy||e.match_length===x&&4096<e.strstart-e.match_start)&&(e.match_length=x-1)),e.prev_length>=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<<e.hash_shift^e.window[e.strstart+x-1])&e.hash_mask,r=e.prev[e.strstart&e.w_mask]=e.head[e.ins_h],e.head[e.ins_h]=e.strstart),0!=--e.prev_length;);if(e.match_available=0,e.match_length=x-1,e.strstart++,n&&(N(e,!1),0===e.strm.avail_out))return A}else if(e.match_available){if((n=u._tr_tally(e,0,e.window[e.strstart-1]))&&N(e,!1),e.strstart++,e.lookahead--,0===e.strm.avail_out)return A}else e.match_available=1,e.strstart++,e.lookahead--}return e.match_available&&(n=u._tr_tally(e,0,e.window[e.strstart-1]),e.match_available=0),e.insert=e.strstart<x-1?e.strstart:x-1,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}function M(e,t,r,n,i){this.good_length=e,this.max_lazy=t,this.nice_length=r,this.max_chain=n,this.func=i}function H(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=v,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new c.Buf16(2*w),this.dyn_dtree=new c.Buf16(2*(2*a+1)),this.bl_tree=new c.Buf16(2*(2*o+1)),D(this.dyn_ltree),D(this.dyn_dtree),D(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new c.Buf16(k+1),this.heap=new c.Buf16(2*s+1),D(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new c.Buf16(2*s+1),D(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function G(e){var t;return e&&e.state?(e.total_in=e.total_out=0,e.data_type=i,(t=e.state).pending=0,t.pending_out=0,t.wrap<0&&(t.wrap=-t.wrap),t.status=t.wrap?C:E,e.adler=2===t.wrap?0:1,t.last_flush=l,u._tr_init(t),m):R(e,_)}function K(e){var t=G(e);return t===m&&function(e){e.window_size=2*e.w_size,D(e.head),e.max_lazy_match=h[e.level].max_lazy,e.good_match=h[e.level].good_length,e.nice_match=h[e.level].nice_length,e.max_chain_length=h[e.level].max_chain,e.strstart=0,e.block_start=0,e.lookahead=0,e.insert=0,e.match_length=e.prev_length=x-1,e.match_available=0,e.ins_h=0}(e.state),t}function Y(e,t,r,n,i,s){if(!e)return _;var a=1;if(t===g&&(t=6),n<0?(a=0,n=-n):15<n&&(a=2,n-=16),i<1||y<i||r!==v||n<8||15<n||t<0||9<t||s<0||b<s)return R(e,_);8===n&&(n=9);var o=new H;return(e.state=o).strm=e,o.wrap=a,o.gzhead=null,o.w_bits=n,o.w_size=1<<o.w_bits,o.w_mask=o.w_size-1,o.hash_bits=i+7,o.hash_size=1<<o.hash_bits,o.hash_mask=o.hash_size-1,o.hash_shift=~~((o.hash_bits+x-1)/x),o.window=new c.Buf8(2*o.w_size),o.head=new c.Buf16(o.hash_size),o.prev=new c.Buf16(o.w_size),o.lit_bufsize=1<<i+6,o.pending_buf_size=4*o.lit_bufsize,o.pending_buf=new c.Buf8(o.pending_buf_size),o.d_buf=1*o.lit_bufsize,o.l_buf=3*o.lit_bufsize,o.level=t,o.strategy=s,o.method=r,K(e)}h=[new M(0,0,0,0,function(e,t){var r=65535;for(r>e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5<t||t<0)return e?R(e,_):_;if(n=e.state,!e.output||!e.input&&0!==e.avail_in||666===n.status&&t!==f)return R(e,0===e.avail_out?-5:_);if(n.strm=e,r=n.last_flush,n.last_flush=t,n.status===C)if(2===n.wrap)e.adler=0,U(n,31),U(n,139),U(n,8),n.gzhead?(U(n,(n.gzhead.text?1:0)+(n.gzhead.hcrc?2:0)+(n.gzhead.extra?4:0)+(n.gzhead.name?8:0)+(n.gzhead.comment?16:0)),U(n,255&n.gzhead.time),U(n,n.gzhead.time>>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else{var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindex<n.gzhead.name.length?255&n.gzhead.name.charCodeAt(n.gzindex++):0,U(n,s)}while(0!==s);n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindex<n.gzhead.comment.length?255&n.gzhead.comment.charCodeAt(n.gzindex++):0,U(n,s)}while(0!==s);n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0<e.strstart&&(n=a[i=e.strstart-1])===a[++i]&&n===a[++i]&&n===a[++i]){s=e.strstart+S;do{}while(n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&n===a[++i]&&i<s);e.match_length=S-(s-i),e.match_length>e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0<n.wrap&&(n.wrap=-n.wrap),0!==n.pending?m:1)},r.deflateEnd=function(e){var t;return e&&e.state?(t=e.state.status)!==C&&69!==t&&73!==t&&91!==t&&103!==t&&t!==E&&666!==t?R(e,_):(e.state=null,t===E?R(e,-3):m):_},r.deflateSetDictionary=function(e,t){var r,n,i,s,a,o,h,u,l=t.length;if(!e||!e.state)return _;if(2===(s=(r=e.state).wrap)||1===s&&r.status!==C||r.lookahead)return _;for(1===s&&(e.adler=d(e.adler,t,l,0)),r.wrap=0,l>=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<<r.hash_shift^r.window[n+x-1])&r.hash_mask,r.prev[n&r.w_mask]=r.head[r.ins_h],r.head[r.ins_h]=n,n++,--i;);r.strstart=n,r.lookahead=x-1,j(r)}return r.strstart+=r.lookahead,r.block_start=r.strstart,r.insert=r.lookahead,r.lookahead=0,r.match_length=r.prev_length=x-1,r.match_available=0,e.next_in=o,e.input=h,e.avail_in=a,r.wrap=s,m},r.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./messages":51,"./trees":52}],47:[function(e,t,r){"use strict";t.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],48:[function(e,t,r){"use strict";t.exports=function(e,t){var r,n,i,s,a,o,h,u,l,f,c,d,p,m,_,g,b,v,y,w,k,x,S,z,C;r=e.state,n=e.next_in,z=e.input,i=n+(e.avail_in-5),s=e.next_out,C=e.output,a=s-(t-e.avail_out),o=s+(e.avail_out-257),h=r.dmax,u=r.wsize,l=r.whave,f=r.wnext,c=r.window,d=r.hold,p=r.bits,m=r.lencode,_=r.distcode,g=(1<<r.lenbits)-1,b=(1<<r.distbits)-1;e:do{p<15&&(d+=z[n++]<<p,p+=8,d+=z[n++]<<p,p+=8),v=m[d&g];t:for(;;){if(d>>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<<y)-1)];continue t}if(32&y){r.mode=12;break e}e.msg="invalid literal/length code",r.mode=30;break e}w=65535&v,(y&=15)&&(p<y&&(d+=z[n++]<<p,p+=8),w+=d&(1<<y)-1,d>>>=y,p-=y),p<15&&(d+=z[n++]<<p,p+=8,d+=z[n++]<<p,p+=8),v=_[d&b];r:for(;;){if(d>>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<<y)-1)];continue r}e.msg="invalid distance code",r.mode=30;break e}if(k=65535&v,p<(y&=15)&&(d+=z[n++]<<p,(p+=8)<y&&(d+=z[n++]<<p,p+=8)),h<(k+=d&(1<<y)-1)){e.msg="invalid distance too far back",r.mode=30;break e}if(d>>>=y,p-=y,(y=s-a)<k){if(l<(y=k-y)&&r.sane){e.msg="invalid distance too far back",r.mode=30;break e}if(S=c,(x=0)===f){if(x+=u-y,y<w){for(w-=y;C[s++]=c[x++],--y;);x=s-k,S=C}}else if(f<y){if(x+=u+f-y,(y-=f)<w){for(w-=y;C[s++]=c[x++],--y;);if(x=0,f<w){for(w-=y=f;C[s++]=c[x++],--y;);x=s-k,S=C}}}else if(x+=f-y,y<w){for(w-=y;C[s++]=c[x++],--y;);x=s-k,S=C}for(;2<w;)C[s++]=S[x++],C[s++]=S[x++],C[s++]=S[x++],w-=3;w&&(C[s++]=S[x++],1<w&&(C[s++]=S[x++]))}else{for(x=s-k;C[s++]=C[x++],C[s++]=C[x++],C[s++]=C[x++],2<(w-=3););w&&(C[s++]=C[x++],1<w&&(C[s++]=C[x++]))}break}}break}}while(n<i&&s<o);n-=w=p>>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n<i?i-n+5:5-(n-i),e.avail_out=s<o?o-s+257:257-(s-o),r.hold=d,r.bits=p}},{}],49:[function(e,t,r){"use strict";var I=e("../utils/common"),O=e("./adler32"),B=e("./crc32"),R=e("./inffast"),T=e("./inftrees"),D=1,F=2,N=0,U=-2,P=1,n=852,i=592;function L(e){return(e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15<t)?U:(null!==n.window&&n.wbits!==t&&(n.window=null),n.wrap=r,n.wbits=t,o(e))):U}function u(e,t){var r,n;return e?(n=new s,(e.state=n).window=null,(r=h(e,t))!==N&&(e.state=null),r):U}var l,f,c=!0;function j(e){if(c){var t;for(l=new I.Buf32(512),f=new I.Buf32(32),t=0;t<144;)e.lens[t++]=8;for(;t<256;)e.lens[t++]=9;for(;t<280;)e.lens[t++]=7;for(;t<288;)e.lens[t++]=8;for(T(D,e.lens,0,288,l,0,e.work,{bits:9}),t=0;t<32;)e.lens[t++]=5;T(F,e.lens,0,32,f,0,e.work,{bits:5}),c=!1}e.lencode=l,e.lenbits=9,e.distcode=f,e.distbits=5}function Z(e,t,r,n){var i,s=e.state;return null===s.window&&(s.wsize=1<<s.wbits,s.wnext=0,s.whave=0,s.window=new I.Buf8(s.wsize)),n>=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave<s.wsize&&(s.whave+=i))),0}r.inflateReset=o,r.inflateReset2=h,r.inflateResetKeep=a,r.inflateInit=function(e){return u(e,15)},r.inflateInit2=u,r.inflate=function(e,t){var r,n,i,s,a,o,h,u,l,f,c,d,p,m,_,g,b,v,y,w,k,x,S,z,C=0,E=new I.Buf8(4),A=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!e||!e.state||!e.output||!e.input&&0!==e.avail_in)return U;12===(r=e.state).mode&&(r.mode=13),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,f=o,c=h,x=N;e:for(;;)switch(r.mode){case P:if(0===r.wrap){r.mode=13;break}for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(2&r.wrap&&35615===u){E[r.check=0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<<k,e.adler=r.check=1,r.mode=512&u?10:12,l=u=0;break;case 2:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(r.flags=u,8!=(255&r.flags)){e.msg="unknown compression method",r.mode=30;break}if(57344&r.flags){e.msg="unknown header flags set",r.mode=30;break}r.head&&(r.head.text=u>>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.head&&(r.head.time=u),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.head&&(r.head.xflags=255&u,r.head.os=u>>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.length=u,r.head&&(r.head.extra_len=u),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d<o;);if(512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,k)break e}else r.head&&(r.head.name=null);r.length=0,r.mode=8;case 8:if(4096&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.comment+=String.fromCharCode(k)),k&&d<o;);if(512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,k)break e}else r.head&&(r.head.comment=null);r.mode=9;case 9:if(512&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u!==(65535&r.check)){e.msg="header crc mismatch",r.mode=30;break}l=u=0}r.head&&(r.head.hcrc=r.flags>>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}e.adler=r.check=L(u),l=u=0,r.mode=11;case 11:if(0===r.havedict)return e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,2;e.adler=r.check=1,r.mode=12;case 12:if(5===t||6===t)break e;case 13:if(r.last){u>>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}switch(r.last=1&u,l-=1,3&(u>>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if((65535&u)!=(u>>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o<d&&(d=o),h<d&&(d=h),0===d)break e;I.arraySet(i,n,s,d,a),o-=d,s+=d,h-=d,a+=d,r.length-=d;break}r.mode=12;break;case 17:for(;l<14;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(r.nlen=257+(31&u),u>>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286<r.nlen||30<r.ndist){e.msg="too many length or distance symbols",r.mode=30;break}r.have=0,r.mode=18;case 18:for(;r.have<r.ncode;){for(;l<3;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.lens[A[r.have++]]=7&u,u>>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have<r.nlen+r.ndist;){for(;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(b<16)u>>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u>>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}l-=_,k=0,d=3+(7&(u>>>=_)),u>>>=3,l-=3}else{for(z=_+7;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}l-=_,k=0,d=11+(127&(u>>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<<r.lenbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(g&&0==(240&g)){for(v=_,y=g,w=b;g=(C=r.lencode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}u>>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.length+=u&(1<<r.extra)-1,u>>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<<r.distbits)-1])>>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(0==(240&g)){for(v=_,y=g,w=b;g=(C=r.distcode[w+((u&(1<<v+y)-1)>>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}u>>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l<z;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}r.offset+=u&(1<<r.extra)-1,u>>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(h<d&&(d=h),h-=d,r.length-=d;i[a++]=m[p++],--d;);0===r.length&&(r.mode=21);break;case 26:if(0===h)break e;i[a++]=r.length,h--,r.mode=21;break;case 27:if(r.wrap){for(;l<32;){if(0===o)break e;o--,u|=n[s++]<<l,l+=8}if(c-=h,e.total_out+=c,r.total+=c,c&&(e.adler=r.check=r.flags?B(r.check,i,c,a-c):O(r.check,i,c,a-c)),c=h,(r.flags?u:L(u))!==r.check){e.msg="incorrect data check",r.mode=30;break}l=u=0}r.mode=28;case 28:if(r.wrap&&r.flags){for(;l<32;){if(0===o)break e;o--,u+=n[s++]<<l,l+=8}if(u!==(4294967295&r.total)){e.msg="incorrect length check",r.mode=30;break}l=u=0}r.mode=29;case 29:x=1;break e;case 30:x=-3;break e;case 31:return-4;case 32:default:return U}return e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,(r.wsize||c!==e.avail_out&&r.mode<30&&(r.mode<27||4!==t))&&Z(e,e.output,e.next_out,c-e.avail_out)?(r.mode=31,-4):(f-=e.avail_in,c-=e.avail_out,e.total_in+=f,e.total_out+=c,r.total+=c,r.wrap&&c&&(e.adler=r.check=r.flags?B(r.check,i,c,e.next_out-c):O(r.check,i,c,e.next_out-c)),e.data_type=r.bits+(r.last?64:0)+(12===r.mode?128:0)+(20===r.mode||15===r.mode?256:0),(0==f&&0===c||4===t)&&x===N&&(x=-5),x)},r.inflateEnd=function(e){if(!e||!e.state)return U;var t=e.state;return t.window&&(t.window=null),e.state=null,N},r.inflateGetHeader=function(e,t){var r;return e&&e.state?0==(2&(r=e.state).wrap)?U:((r.head=t).done=!1,N):U},r.inflateSetDictionary=function(e,t){var r,n=t.length;return e&&e.state?0!==(r=e.state).wrap&&11!==r.mode?U:11===r.mode&&O(1,t,n,0)!==r.check?-3:Z(e,t,n,n)?(r.mode=31,-4):(r.havedict=1,N):U},r.inflateInfo="pako inflate (from Nodeca project)"},{"../utils/common":41,"./adler32":43,"./crc32":45,"./inffast":48,"./inftrees":50}],50:[function(e,t,r){"use strict";var D=e("../utils/common"),F=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],N=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],U=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],P=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];t.exports=function(e,t,r,n,i,s,a,o){var h,u,l,f,c,d,p,m,_,g=o.bits,b=0,v=0,y=0,w=0,k=0,x=0,S=0,z=0,C=0,E=0,A=null,I=0,O=new D.Buf16(16),B=new D.Buf16(16),R=null,T=0;for(b=0;b<=15;b++)O[b]=0;for(v=0;v<n;v++)O[t[r+v]]++;for(k=g,w=15;1<=w&&0===O[w];w--);if(w<k&&(k=w),0===w)return i[s++]=20971520,i[s++]=20971520,o.bits=1,0;for(y=1;y<w&&0===O[y];y++);for(k<y&&(k=y),b=z=1;b<=15;b++)if(z<<=1,(z-=O[b])<0)return-1;if(0<z&&(0===e||1!==w))return-1;for(B[1]=0,b=1;b<15;b++)B[b+1]=B[b]+O[b];for(v=0;v<n;v++)0!==t[r+v]&&(a[B[t[r+v]]++]=v);if(d=0===e?(A=R=a,19):1===e?(A=F,I-=257,R=N,T-=257,256):(A=U,R=P,-1),b=y,c=s,S=v=E=0,l=-1,f=(C=1<<(x=k))-1,1===e&&852<C||2===e&&592<C)return 1;for(;;){for(p=b-S,_=a[v]<d?(m=0,a[v]):a[v]>d?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<<b-S,y=u=1<<x;i[c+(E>>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<<b-1;E&h;)h>>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k<b&&(E&f)!==l){for(0===S&&(S=k),c+=y,z=1<<(x=b-S);x+S<w&&!((z-=O[x+S])<=0);)x++,z<<=1;if(C+=1<<x,1===e&&852<C||2===e&&592<C)return 1;i[l=E&f]=k<<24|x<<16|c-s|0}}return 0!==E&&(i[c+E]=b-S<<24|64<<16|0),o.bits=k,0}},{"../utils/common":41}],51:[function(e,t,r){"use strict";t.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],52:[function(e,t,r){"use strict";var i=e("../utils/common"),o=0,h=1;function n(e){for(var t=e.length;0<=--t;)e[t]=0}var s=0,a=29,u=256,l=u+1+a,f=30,c=19,_=2*l+1,g=15,d=16,p=7,m=256,b=16,v=17,y=18,w=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],k=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],x=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],S=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],z=new Array(2*(l+2));n(z);var C=new Array(2*f);n(C);var E=new Array(512);n(E);var A=new Array(256);n(A);var I=new Array(a);n(I);var O,B,R,T=new Array(f);function D(e,t,r,n,i){this.static_tree=e,this.extra_bits=t,this.extra_base=r,this.elems=n,this.max_length=i,this.has_stree=e&&e.length}function F(e,t){this.dyn_tree=e,this.max_code=0,this.stat_desc=t}function N(e){return e<256?E[e]:E[256+(e>>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<<e.bi_valid&65535,U(e,e.bi_buf),e.bi_buf=t>>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<<e.bi_valid&65535,e.bi_valid+=r)}function L(e,t,r){P(e,r[2*t],r[2*t+1])}function j(e,t){for(var r=0;r|=1&e,e>>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t<l;t++)e.dyn_ltree[2*t]=0;for(t=0;t<f;t++)e.dyn_dtree[2*t]=0;for(t=0;t<c;t++)e.bl_tree[2*t]=0;e.dyn_ltree[2*m]=1,e.opt_len=e.static_len=0,e.last_lit=e.matches=0}function M(e){8<e.bi_valid?U(e,e.bi_buf):0<e.bi_valid&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0}function H(e,t,r,n){var i=2*t,s=2*r;return e[i]<e[s]||e[i]===e[s]&&n[t]<=n[r]}function G(e,t,r){for(var n=e.heap[r],i=r<<1;i<=e.heap_len&&(i<e.heap_len&&H(t,e.heap[i+1],e.heap[i],e.depth)&&i++,!H(t,n,e.heap[i],e.depth));)e.heap[r]=e.heap[i],r=i,i<<=1;e.heap[r]=n}function K(e,t,r){var n,i,s,a,o=0;if(0!==e.last_lit)for(;n=e.pending_buf[e.d_buf+2*o]<<8|e.pending_buf[e.d_buf+2*o+1],i=e.pending_buf[e.l_buf+o],o++,0===n?L(e,i,t):(L(e,(s=A[i])+u+1,t),0!==(a=w[s])&&P(e,i-=I[s],a),L(e,s=N(--n),r),0!==(a=k[s])&&P(e,n-=T[s],a)),o<e.last_lit;);L(e,m,t)}function Y(e,t){var r,n,i,s=t.dyn_tree,a=t.stat_desc.static_tree,o=t.stat_desc.has_stree,h=t.stat_desc.elems,u=-1;for(e.heap_len=0,e.heap_max=_,r=0;r<h;r++)0!==s[2*r]?(e.heap[++e.heap_len]=u=r,e.depth[r]=0):s[2*r+1]=0;for(;e.heap_len<2;)s[2*(i=e.heap[++e.heap_len]=u<2?++u:0)]=1,e.depth[i]=0,e.opt_len--,o&&(e.static_len-=a[2*i+1]);for(t.max_code=u,r=e.heap_len>>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u<n||(e.bl_count[s]++,a=0,d<=n&&(a=c[n-d]),o=h[2*n],e.opt_len+=o*(s+a),f&&(e.static_len+=o*(l[2*n+1]+a)));if(0!==m){do{for(s=p-1;0===e.bl_count[s];)s--;e.bl_count[s]--,e.bl_count[s+1]+=2,e.bl_count[p]--,m-=2}while(0<m);for(s=p;0!==s;s--)for(n=e.bl_count[s];0!==n;)u<(i=e.heap[--r])||(h[2*i+1]!==s&&(e.opt_len+=(s-h[2*i+1])*h[2*i],h[2*i+1]=s),n--)}}(e,t),Z(s,u,e.bl_count)}function X(e,t,r){var n,i,s=-1,a=t[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),t[2*(r+1)+1]=65535,n=0;n<=r;n++)i=a,a=t[2*(n+1)+1],++o<h&&i===a||(o<u?e.bl_tree[2*i]+=o:0!==i?(i!==s&&e.bl_tree[2*i]++,e.bl_tree[2*b]++):o<=10?e.bl_tree[2*v]++:e.bl_tree[2*y]++,s=i,u=(o=0)===a?(h=138,3):i===a?(h=6,3):(h=7,4))}function V(e,t,r){var n,i,s=-1,a=t[1],o=0,h=7,u=4;for(0===a&&(h=138,u=3),n=0;n<=r;n++)if(i=a,a=t[2*(n+1)+1],!(++o<h&&i===a)){if(o<u)for(;L(e,i,e.bl_tree),0!=--o;);else 0!==i?(i!==s&&(L(e,i,e.bl_tree),o--),L(e,b,e.bl_tree),P(e,o-3,2)):o<=10?(L(e,v,e.bl_tree),P(e,o-3,3)):(L(e,y,e.bl_tree),P(e,o-11,7));s=i,u=(o=0)===a?(h=138,3):i===a?(h=6,3):(h=7,4)}}n(T);var q=!1;function J(e,t,r,n){P(e,(s<<1)+(n?1:0),3),function(e,t,r,n){M(e),n&&(U(e,r),U(e,~r)),i.arraySet(e.pending_buf,e.window,t,r,e.pending),e.pending+=r}(e,t,r,!0)}r._tr_init=function(e){q||(function(){var e,t,r,n,i,s=new Array(g+1);for(n=r=0;n<a-1;n++)for(I[n]=r,e=0;e<1<<w[n];e++)A[r++]=n;for(A[r-1]=n,n=i=0;n<16;n++)for(T[n]=i,e=0;e<1<<k[n];e++)E[i++]=n;for(i>>=7;n<f;n++)for(T[n]=i<<7,e=0;e<1<<k[n]-7;e++)E[256+i++]=n;for(t=0;t<=g;t++)s[t]=0;for(e=0;e<=143;)z[2*e+1]=8,e++,s[8]++;for(;e<=255;)z[2*e+1]=9,e++,s[9]++;for(;e<=279;)z[2*e+1]=7,e++,s[7]++;for(;e<=287;)z[2*e+1]=8,e++,s[8]++;for(Z(z,l+1,s),e=0;e<f;e++)C[2*e+1]=5,C[2*e]=j(e,5);O=new D(z,w,u+1,l,g),B=new D(C,k,0,f,g),R=new D(new Array(0),x,0,c,p)}(),q=!0),e.l_desc=new F(e.dyn_ltree,O),e.d_desc=new F(e.dyn_dtree,B),e.bl_desc=new F(e.bl_tree,R),e.bi_buf=0,e.bi_valid=0,W(e)},r._tr_stored_block=J,r._tr_flush_block=function(e,t,r,n){var i,s,a=0;0<e.level?(2===e.strm.data_type&&(e.strm.data_type=function(e){var t,r=4093624447;for(t=0;t<=31;t++,r>>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t<u;t++)if(0!==e.dyn_ltree[2*t])return h;return o}(e)),Y(e,e.l_desc),Y(e,e.d_desc),a=function(e){var t;for(X(e,e.dyn_ltree,e.l_desc.max_code),X(e,e.dyn_dtree,e.d_desc.max_code),Y(e,e.bl_desc),t=c-1;3<=t&&0===e.bl_tree[2*S[t]+1];t--);return e.opt_len+=3*(t+1)+5+5+4,t}(e),i=e.opt_len+3+7>>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i<n;i++)P(e,e.bl_tree[2*S[i]+1],3);V(e,e.dyn_ltree,t-1),V(e,e.dyn_dtree,r-1)}(e,e.l_desc.max_code+1,e.d_desc.max_code+1,a+1),K(e,e.dyn_ltree,e.dyn_dtree)),W(e),n&&M(e)},r._tr_tally=function(e,t,r){return e.pending_buf[e.d_buf+2*e.last_lit]=t>>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r<t.length;r++)t[r]=arguments[r+1];var n={callback:e,args:t};return h[o]=n,i(o),o++},e.clearImmediate=f}function f(e){delete h[e]}function c(e){if(u)setTimeout(c,0,e);else{var t=h[e];if(t){u=!0;try{!function(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}(t)}finally{f(e),u=!1}}}}function d(e){e.source===r&&"string"==typeof e.data&&0===e.data.indexOf(a)&&c(+e.data.slice(a.length))}}("undefined"==typeof self?void 0===e?this:e:self)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[10])(10)});