@clearmist-labs/comic-archive-handler 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/index.d.mts +584 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2157 -0
- package/dist/index.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/docs/API.md +809 -0
- package/package.json +82 -0
- package/schemas/ComicInfo v2.1.xsd +127 -0
- package/schemas/LICENSE-MetronInfo.txt +504 -0
- package/schemas/MetronInfo v1.1.xsd +355 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Sanders
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @clearmist-labs/comic-archive-handler
|
|
2
|
+
|
|
3
|
+
A toolkit for detecting, converting, and manipulating comic book archives (CBAS/CBZ/CB7/CBR/CBT/CBA).
|
|
4
|
+
|
|
5
|
+
- **Detect** archive types and container formats
|
|
6
|
+
- **Convert** between zip/rar/tar/asar/7z containers
|
|
7
|
+
- **Read & write** ComicInfo.xml/MetronInfo.xml metadata
|
|
8
|
+
- **Validate** metadata with full field coverage of both schemas and XSD-backed validation
|
|
9
|
+
- **Re-encode** page images to different formats
|
|
10
|
+
- **Hash** archives perceptually and by content
|
|
11
|
+
- **Rename** pages sequentially
|
|
12
|
+
- **Strip** non-essential files from archives
|
|
13
|
+
- **Extract** archives to a plain directory of files
|
|
14
|
+
- **Benchmark** archive/image format combinations for creation speed, random read speed, and size
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install @clearmist-labs/comic-archive-handler
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import fs from 'node:fs/promises';
|
|
26
|
+
import cah from '@clearmist-labs/comic-archive-handler';
|
|
27
|
+
// or: import { detectArchiveType, convertArchive } from '@clearmist-labs/comic-archive-handler';
|
|
28
|
+
|
|
29
|
+
/* Convert a cbz to cba buffer */
|
|
30
|
+
const type = await cah.detectArchiveType('/path/to/comic.cbz'); // 'zip'
|
|
31
|
+
const asarBuffer = await cah.convertArchive('/path/to/comic.cbz', 'asar');
|
|
32
|
+
|
|
33
|
+
/* Inspect its metadata */
|
|
34
|
+
const { present, schema } = await cah.hasComicMetadata(asarBuffer);
|
|
35
|
+
const { metadata } = await cah.readArchiveMetadata(asarBuffer);
|
|
36
|
+
|
|
37
|
+
/* Add some more metadata then validate we did it correctly */
|
|
38
|
+
const withMetadata = await cah.addMetadataToArchive(asarBuffer, { title: 'Example', series: 'Example Series', number: '1' }, 'ComicInfo');
|
|
39
|
+
const { valid, issues } = await cah.validateMetadataXml(cah.metadataToComicInfoXml({ title: 'Example' }), 'ComicInfo');
|
|
40
|
+
|
|
41
|
+
/* Make sure our images use the standard naming format */
|
|
42
|
+
const renamed = await cah.renameArchiveImagesSequentially(withMetadata); // P00001.jpg, P00002.jpg, ...
|
|
43
|
+
|
|
44
|
+
/* Get rid of anything we don't recognize */
|
|
45
|
+
const stripped = await cah.stripNonEssentialFiles(renamed); // keeps only images + .xml
|
|
46
|
+
|
|
47
|
+
/* Convert all of the images to WebP */
|
|
48
|
+
const webp = await cah.convertArchiveImages(stripped, 'webp', { webp: { quality: 92, effort: 6, smartSubsample: true } });
|
|
49
|
+
|
|
50
|
+
/* Generate hashes */
|
|
51
|
+
const contentHash = await cah.sha256Archive(webp); // For asar this hashes only the content region (not the header/index).
|
|
52
|
+
const pageHash = await cah.sha256ArchiveEntry(webp, 'P00001.webp');
|
|
53
|
+
const phash = await cah.computeArchiveImagePHash(webp, 'P00001.webp'); // 64-bit bigint
|
|
54
|
+
|
|
55
|
+
/* Write the buffer to disk */
|
|
56
|
+
await fs.writeFile('/path/to/output.cba', webp);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Streaming large archives to disk
|
|
60
|
+
|
|
61
|
+
Every archive-producing function accepts an `output` option (a file path or a `Writable`) to stream the result directly there instead of collecting it into a `Buffer`:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
await cah.convertArchive('/path/to/large.cbz', 'zip', { output: '/path/to/output.cbz' });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Benchmarking archive/image format combinations
|
|
68
|
+
|
|
69
|
+
`benchmarkArchive` extracts a comic archive, generates every writable-container × page-image-format combination (12 archives: zip/tar/asar/7z × webp/png/jpg), times each one's creation and random single-page read speed, and writes the generated archives plus a markdown report to a timestamped `reports/<datetime>/` directory:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const result = await cah.benchmarkArchive('/path/to/comic.cbz');
|
|
73
|
+
console.log(result.reportPath); // ./reports/2026-09-12T19-47-00Z/report.md
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
See the [API reference](docs/API.md) for descriptions, options, examples, exported types, and custom errors.
|
|
79
|
+
|
|
80
|
+
Most runtime exports are available both as named exports and as properties on the default export. Type-only exports are available to TypeScript consumers but are not runtime properties of the default export.
|
|
81
|
+
|
|
82
|
+
## File format support
|
|
83
|
+
|
|
84
|
+
### Archive containers
|
|
85
|
+
|
|
86
|
+
| Format | Extension | Detectable | Readable | Writable |
|
|
87
|
+
| ------ | --------- | ---------- | -------- | -------- |
|
|
88
|
+
| Zip | .cbz | ✅ | ✅ | ✅ |
|
|
89
|
+
| Asar | .cbas | ✅ | ✅ | ✅ |
|
|
90
|
+
| 7z | .cb7 | ✅ | ✅ | ✅ |
|
|
91
|
+
| Tar | .cbt | ✅ | ✅ | ✅ |
|
|
92
|
+
| Rar | .cbr | ✅ | ✅ | ❌ |
|
|
93
|
+
| Ace | .cba | ✅ | ❌ | ❌ |
|
|
94
|
+
|
|
95
|
+
Ace is a dead format and should never be used.
|
|
96
|
+
|
|
97
|
+
Rar is closed sourced. Writing rar files requires commercial software. I strongly suggest not using CBR as your archive choice.
|
|
98
|
+
|
|
99
|
+
Asar supports fully streaming reads: individual pages can be pulled out via direct byte-range access without extracting or decompressing the whole archive, making it the fastest option for random page access. This makes it the best choice for remote digital libraries; read and send a single page to a client without extracting the entire archive to disk.
|
|
100
|
+
|
|
101
|
+
### Page images
|
|
102
|
+
|
|
103
|
+
| Format | Detectable | Readable | Writable | Archival quality |
|
|
104
|
+
| ------ | ---------- | -------- | -------- | ---------------- |
|
|
105
|
+
| WebP | ✅ | ✅ | ✅ | Excellent |
|
|
106
|
+
| PNG | ✅ | ✅ | ✅ | Excellent |
|
|
107
|
+
| JPEG | ✅ | ✅ | ✅ | Good |
|
|
108
|
+
| GIF | ✅ | ✅ | ❌ | Poor |
|
|
109
|
+
| BMP | ✅ | ✅ | ❌ | Poor |
|
|
110
|
+
| TIFF | ✅ | ✅ | ❌ | Poor |
|
|
111
|
+
|
|
112
|
+
The best choice for image quality and size is WebP at quality 90 to 95.
|
|
113
|
+
|
|
114
|
+
The best choice for wide compatibility across very old devices is JPG.
|
|
115
|
+
|
|
116
|
+
GIF, BMP, and TIFF are intentionally not included as writable because of their poor image quality or large file size.
|
|
117
|
+
|
|
118
|
+
## Notes and known limitations
|
|
119
|
+
|
|
120
|
+
- **ACE (CBA) is entirely unsupported.** ACE archives are still detected (`detectArchiveType`/`isAce`), but every read, write, and conversion operation throws `UnsupportedOperationError`: ACE is a dead format that hasn't been updated since 2011, has multiple known security vulnerabilities, and — unlike RAR or 7z — has no maintained JS/WASM decoder or bundleable cross-platform binary to build support on.
|
|
121
|
+
- **RAR is read-only.** The UnRAR source license permits decompression only, not building a compatible compressor, so `convertArchive(input, 'rar')` always throws `UnsupportedOperationError`. RAR reading uses [`node-unrar-js`](https://github.com/YuJianrong/node-unrar.js) (pure JS/WASM, no system binary); note it decodes the whole archive into memory at once rather than streaming entries individually — a limitation of that library, not this package. It has been lightly maintained since 2023; [`electron-unrar-js`](https://www.npmjs.com/package/electron-unrar-js) is a maintained fork worth watching as a fallback.
|
|
122
|
+
- **7z read/write** uses the standalone `7zzs` binary bundled by [`7zip-bin-full`](https://www.npmjs.com/package/7zip-bin-full), spawned directly (not through `node-7z`, which has no way to control the child process's working directory). Both directions round-trip through a temporary staging directory since the 7-Zip CLI operates on real files, not in-memory buffers.
|
|
123
|
+
- **asar** reads are fully streaming (direct byte-range reads against the archive, no extraction), but writes require a staging directory since `@electron/asar`'s `createPackage` API only accepts a source directory on disk.
|
|
124
|
+
- Any operation that needs a staging directory (asar writes, any 7z operation) accepts a `tempDir` option; if omitted, a directory under `os.tmpdir()` is used, and a clear `FilesystemAccessError` is thrown if no writable directory is available.
|
|
125
|
+
- Metadata conversion between the canonical schema and ComicInfo.xml/MetronInfo.xml is intentionally lossy in both directions — see the field-mapping table in `src/metadata/schema.ts`.
|
|
126
|
+
- PNG conversion's `quality` option only has an effect when `palette: true` is also set (an upstream `sharp`/libvips behavior).
|
|
127
|
+
|
|
128
|
+
## Official schemas
|
|
129
|
+
|
|
130
|
+
These are separate works and are included here to help us validate schemas against the officialy published schemas.
|
|
131
|
+
|
|
132
|
+
- [ComicInfo.xml](https://github.com/anansi-project/comicinfo) / MIT
|
|
133
|
+
- [MetronInfo.xml](https://github.com/Metron-Project/metroninfo) / LGPL-2.1
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|