clean_epub_css.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>EPUB CSS 清理工具 (Web版)</title>
  7. <style>
  8. body {
  9. font-family: sans-serif;
  10. line-height: 1.6;
  11. padding: 20px;
  12. max-width: 600px;
  13. margin: 20px auto;
  14. background-color: #f4f4f4;
  15. border: 1px solid #ccc;
  16. border-radius: 8px;
  17. }
  18. h1 {
  19. text-align: center;
  20. color: #333;
  21. }
  22. #drop-area {
  23. border: 2px dashed #ccc;
  24. border-radius: 5px;
  25. padding: 30px;
  26. text-align: center;
  27. background-color: #fff;
  28. margin-bottom: 20px;
  29. cursor: pointer;
  30. }
  31. #drop-area.highlight {
  32. border-color: dodgerblue;
  33. }
  34. #drop-area p {
  35. margin: 0;
  36. color: #555;
  37. }
  38. #fileInput {
  39. display: none; /* Hide default input, use drop area or label */
  40. }
  41. label.file-label {
  42. display: inline-block;
  43. padding: 10px 15px;
  44. background-color: dodgerblue;
  45. color: white;
  46. border: none;
  47. border-radius: 4px;
  48. cursor: pointer;
  49. margin-top: 10px;
  50. transition: background-color 0.2s;
  51. }
  52. label.file-label:hover {
  53. background-color: #007ae5;
  54. }
  55. #status {
  56. margin-top: 15px;
  57. padding: 10px;
  58. background-color: #eee;
  59. border-radius: 4px;
  60. min-height: 40px; /* Ensure space for messages */
  61. text-align: center;
  62. font-weight: bold;
  63. }
  64. .status-details {
  65. font-size: 0.9em;
  66. color: #555;
  67. margin-top: 5px;
  68. }
  69. .warning {
  70. color: #a00;
  71. font-weight: bold;
  72. text-align: center;
  73. margin-top: 20px;
  74. padding: 10px;
  75. background-color: #fdd;
  76. border: 1px solid #fbb;
  77. border-radius: 4px;
  78. }
  79. progress {
  80. width: 100%;
  81. margin-top: 10px;
  82. display: none; /* Hidden by default */
  83. }
  84. </style>
  85. <!-- Include JSZip library -->
  86. <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
  87. <!-- Include FileSaver.js library -->
  88. <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
  89. </head>
  90. <body>
  91. <h1>EPUB CSS 清理工具 (Web版)</h1>
  92. <p>将单个 EPUB 文件或包含多个 EPUB 的 ZIP 文件拖拽到下方区域,或点击按钮选择文件,即可移除指定的 CSS 样式。</p>
  93. <div id="drop-area">
  94. <!-- Accept both .epub and .zip -->
  95. <input type="file" id="fileInput" accept=".epub,.zip">
  96. <p>将 EPUB 或 ZIP 文件拖拽到这里</p>
  97. <label for="fileInput" class="file-label">或者选择文件</label>
  98. </div>
  99. <progress id="progressBar" value="0" max="100"></progress>
  100. <div id="status">请选择或拖拽一个 EPUB 或 ZIP 文件。</div>
  101. <div id="statusDetails" class="status-details"></div>
  102. <div class="warning">
  103. <strong>重要提示:</strong> 此工具在你的浏览器中处理文件,不会上传到服务器。处理后的文件需要你手动下载。建议在处理前备份原始文件!处理 ZIP 文件时,会生成一个包含所有处理后(或原始)EPUB 的新 ZIP 文件。
  104. </div>
  105. <script>
  106. // --- Check if libraries loaded ---
  107. if (typeof JSZip === 'undefined' || typeof saveAs === 'undefined') {
  108. const errorMsg = '错误:无法加载必需的库 (JSZip 或 FileSaver)。请检查您的网络连接,并确保没有浏览器插件(如广告拦截器)阻止从 cdnjs.cloudflare.com 加载脚本。然后请刷新页面重试。';
  109. document.getElementById('status').textContent = errorMsg;
  110. document.getElementById('status').style.color = '#a00';
  111. document.getElementById('drop-area').style.display = 'none';
  112. document.querySelector('.warning').textContent = errorMsg;
  113. } else {
  114. // --- Global Variables & Constants ---
  115. const dropArea = document.getElementById('drop-area');
  116. const fileInput = document.getElementById('fileInput');
  117. const statusDiv = document.getElementById('status');
  118. const statusDetailsDiv = document.getElementById('statusDetails');
  119. const progressBar = document.getElementById('progressBar');
  120. const CSS_REMOVE_PATTERN = /((text-indent|line-height|font-size|height|font-family|color)\s*:\s*[^;]*;|display\s*:\s*block\s*;)/ig;
  121. // --- Event Handlers ---
  122. setupDragDropHandlers();
  123. fileInput.addEventListener('change', handleFileSelect, false);
  124. function setupDragDropHandlers() {
  125. ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  126. dropArea.addEventListener(eventName, preventDefaults, false);
  127. document.body.addEventListener(eventName, preventDefaults, false);
  128. });
  129. ['dragenter', 'dragover'].forEach(eventName => {
  130. dropArea.addEventListener(eventName, () => dropArea.classList.add('highlight'), false);
  131. });
  132. ['dragleave', 'drop'].forEach(eventName => {
  133. dropArea.addEventListener(eventName, () => dropArea.classList.remove('highlight'), false);
  134. });
  135. dropArea.addEventListener('drop', handleDrop, false);
  136. }
  137. function preventDefaults(e) {
  138. e.preventDefault();
  139. e.stopPropagation();
  140. }
  141. function handleDrop(e) {
  142. handleFiles(e.dataTransfer.files);
  143. }
  144. function handleFileSelect(e) {
  145. handleFiles(e.target.files);
  146. }
  147. // --- Main File Handling Logic ---
  148. function handleFiles(files) {
  149. if (files.length === 0) return;
  150. const file = files[0]; // Process only the first file
  151. // Check file type
  152. const isEpub = file.name.toLowerCase().endsWith('.epub');
  153. const isZip = file.name.toLowerCase().endsWith('.zip');
  154. if (!isEpub && !isZip) {
  155. updateStatus('错误:请选择一个 .epub 或 .zip 文件。', true);
  156. resetInput();
  157. return;
  158. }
  159. updateStatus(`正在读取文件: ${file.name}...`);
  160. clearStatusDetails();
  161. progressBar.style.display = 'block';
  162. progressBar.value = 0;
  163. const reader = new FileReader();
  164. reader.onload = async (e) => {
  165. const fileContent = e.target.result; // ArrayBuffer
  166. try {
  167. if (isEpub) {
  168. updateStatus(`正在处理 EPUB 文件: ${file.name}...`);
  169. const result = await processEpub(fileContent, file.name);
  170. if (result.blob) {
  171. saveAs(result.blob, result.filename);
  172. updateStatus(`处理完成!已生成 ${result.filename}。`);
  173. } else if (result.modified === false) {
  174. updateStatus('信息:文件无需修改。');
  175. }
  176. resetInput();
  177. } else if (isZip) {
  178. updateStatus(`正在处理 ZIP 压缩包: ${file.name}...`);
  179. await processZipArchive(fileContent, file.name);
  180. }
  181. } catch (error) {
  182. console.error("处理失败:", error);
  183. updateStatus(`处理失败:${error.message}`, true);
  184. resetInput();
  185. }
  186. };
  187. reader.onprogress = updateReadProgress;
  188. reader.onerror = handleReadError;
  189. reader.readAsArrayBuffer(file);
  190. }
  191. function updateReadProgress(e) {
  192. if (e.lengthComputable) {
  193. progressBar.value = Math.round((e.loaded / e.total) * 10); // Reading is ~10%
  194. }
  195. }
  196. function handleReadError() {
  197. updateStatus('错误:读取文件时出错。', true);
  198. resetInput();
  199. }
  200. // --- ZIP Archive Processing ---
  201. async function processZipArchive(zipFileContent, zipFilename) {
  202. let outputZip = new JSZip();
  203. let processedCount = 0;
  204. let failedCount = 0;
  205. let foundCount = 0;
  206. let modifiedCount = 0;
  207. let filePromises = [];
  208. updateStatus(`正在打开 ZIP 压缩包...`);
  209. progressBar.value = 15;
  210. const inputZip = await JSZip.loadAsync(zipFileContent);
  211. const totalFilesInZip = Object.keys(inputZip.files).length;
  212. updateStatus(`在 ZIP 中查找 EPUB 文件...`);
  213. inputZip.forEach((relativePath, zipEntry) => {
  214. if (!zipEntry.dir && relativePath.toLowerCase().endsWith('.epub')) {
  215. foundCount++;
  216. updateStatusDetails(`找到 EPUB: ${relativePath}`);
  217. // Add a promise to process this EPUB entry
  218. const promise = zipEntry.async('arraybuffer')
  219. .then(async (innerEpubContent) => {
  220. try {
  221. // Process the inner EPUB
  222. const result = await processEpub(innerEpubContent, relativePath);
  223. if (result.blob) {
  224. // Add modified EPUB to output ZIP
  225. outputZip.file(result.filename, result.blob);
  226. modifiedCount++;
  227. console.log(`Added cleaned ${result.filename} to output zip.`);
  228. } else {
  229. // Add original EPUB back if not modified
  230. outputZip.file(relativePath, innerEpubContent);
  231. console.log(`Added original ${relativePath} (no changes needed) to output zip.`);
  232. }
  233. processedCount++;
  234. } catch (epubError) {
  235. // Add original EPUB back if processing failed
  236. console.error(`Error processing inner EPUB ${relativePath}:`, epubError);
  237. updateStatusDetails(`处理 ${relativePath} 时出错,将保留原文件。`);
  238. outputZip.file(relativePath, innerEpubContent); // Add original back
  239. failedCount++;
  240. }
  241. })
  242. .catch(extractError => {
  243. // Handle error extracting the inner EPUB content
  244. console.error(`Error extracting inner EPUB ${relativePath} from zip:`, extractError);
  245. updateStatusDetails(`无法从 ZIP 中提取 ${relativePath},已跳过。`);
  246. failedCount++;
  247. })
  248. .finally(() => {
  249. // Update progress based on entries processed (success or fail)
  250. const progress = 15 + Math.round(((processedCount + failedCount) / foundCount) * 60); // Processing EPUBs is ~60%
  251. progressBar.value = Math.min(progress, 75); // Cap at 75 before final packing
  252. });
  253. filePromises.push(promise);
  254. }
  255. // We are currently NOT adding non-EPUB files from the source ZIP to the output ZIP.
  256. // To do so, handle the 'else' case here and add zipEntry data to outputZip.
  257. });
  258. // Wait for all inner EPUB processing promises to complete
  259. await Promise.all(filePromises);
  260. progressBar.value = 80; // Done processing files inside zip
  261. if (foundCount === 0) {
  262. updateStatus('错误:在 ZIP 文件中未找到任何 EPUB 文件。', true);
  263. resetInput();
  264. return;
  265. }
  266. updateStatus(`处理了 ${foundCount} 个 EPUB 文件。正在生成输出 ZIP...`);
  267. statusDetailsDiv.innerHTML += `<br>成功: ${processedCount}, 修改: ${modifiedCount}, 失败/跳过: ${failedCount}`;
  268. progressBar.value = 90;
  269. const outputZipBlob = await outputZip.generateAsync({
  270. type: "blob",
  271. compression: "DEFLATE",
  272. platform: "browser"
  273. }, (metadata) => {
  274. progressBar.value = 90 + Math.round(metadata.percent * 0.1);
  275. });
  276. progressBar.value = 100;
  277. const baseName = zipFilename.substring(0, zipFilename.lastIndexOf('.')) || zipFilename;
  278. const outputZipFilename = `${baseName}_cleaned.zip`;
  279. saveAs(outputZipBlob, outputZipFilename);
  280. updateStatus(`处理完成!已生成包含处理后 EPUB 的 ${outputZipFilename}。`);
  281. resetInput();
  282. }
  283. // --- Single EPUB Processing (Modified to return result) ---
  284. async function processEpub(fileContent, originalFilename) {
  285. // This function now returns an object:
  286. // { blob: Blob, filename: string } if modified
  287. // { modified: false } if no modification needed
  288. // Throws error if processing fails critically
  289. console.log(`Processing EPUB: ${originalFilename}`);
  290. // Note: Progress bar updates inside this function are less meaningful now,
  291. // as the overall progress is handled by the caller (handleFiles or processZipArchive).
  292. // We could pass a progress callback if detailed inner progress is needed.
  293. const zip = await JSZip.loadAsync(fileContent);
  294. const newZip = new JSZip();
  295. let cssFilesFound = false;
  296. let modified = false;
  297. let filePromises = [];
  298. zip.forEach((relativePath, zipEntry) => {
  299. if (zipEntry.dir) { newZip.folder(relativePath); return; }
  300. const options = {
  301. date: zipEntry.date,
  302. unixPermissions: zipEntry.unixPermissions,
  303. dosPermissions: zipEntry.dosPermissions,
  304. comment: zipEntry.comment,
  305. dir: zipEntry.dir,
  306. compression: zipEntry.options?.compression === "STORE" ? "STORE" : "DEFLATE"
  307. };
  308. if (relativePath.toLowerCase() === 'mimetype') {
  309. const promise = zipEntry.async('uint8array').then(data => {
  310. newZip.file(relativePath, data, { ...options, compression: "STORE" });
  311. });
  312. filePromises.push(promise);
  313. } else if (relativePath.toLowerCase().endsWith('.css')) {
  314. cssFilesFound = true;
  315. const promise = zipEntry.async('string').then(originalCss => {
  316. const cleanedCss = originalCss.replace(CSS_REMOVE_PATTERN, '');
  317. let contentToAdd = originalCss;
  318. if (cleanedCss !== originalCss) {
  319. modified = true;
  320. contentToAdd = cleanedCss;
  321. console.log(` Cleaned CSS: ${relativePath}`);
  322. }
  323. newZip.file(relativePath, contentToAdd, options);
  324. }).catch(err => {
  325. console.warn(` Could not process CSS ${relativePath} as text, keeping original. Error: ${err}`);
  326. return zipEntry.async('uint8array').then(originalData => {
  327. newZip.file(relativePath, originalData, options);
  328. });
  329. });
  330. filePromises.push(promise);
  331. } else {
  332. const promise = zipEntry.async('uint8array').then(data => {
  333. newZip.file(relativePath, data, options);
  334. });
  335. filePromises.push(promise);
  336. }
  337. });
  338. await Promise.all(filePromises);
  339. if (!modified) {
  340. console.log(` EPUB ${originalFilename} - No modifications needed.`);
  341. return { modified: false }; // Indicate no changes were made
  342. }
  343. console.log(` Repacking modified EPUB: ${originalFilename}`);
  344. const newEpubBlob = await newZip.generateAsync({
  345. type: "blob",
  346. mimeType: "application/epub+zip",
  347. compression: "DEFLATE",
  348. platform: "browser"
  349. });
  350. const baseName = originalFilename.substring(0, originalFilename.lastIndexOf('.')) || originalFilename;
  351. const newFilename = `${baseName}_cleaned.epub`;
  352. return { blob: newEpubBlob, filename: newFilename }; // Return data for the caller
  353. } // end of processEpub function
  354. // --- UI Update Functions ---
  355. function updateStatus(message, isError = false) {
  356. statusDiv.textContent = message;
  357. statusDiv.style.color = isError ? '#a00' : '#000';
  358. if (isError) {
  359. progressBar.style.display = 'none';
  360. clearStatusDetails();
  361. }
  362. }
  363. function updateStatusDetails(message) {
  364. statusDetailsDiv.innerHTML += message + "<br>";
  365. }
  366. function clearStatusDetails() {
  367. statusDetailsDiv.innerHTML = "";
  368. }
  369. function resetInput() {
  370. fileInput.value = '';
  371. // Hide progress bar after a delay
  372. setTimeout(() => { progressBar.style.display = 'none'; progressBar.value = 0; }, 5000);
  373. }
  374. } // End of the 'else' block for library check
  375. </script>
  376. </body>
  377. </html>