@clearmist-labs/comic-archive-handler 1.3.0 → 1.5.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/docs/API.md CHANGED
@@ -27,7 +27,7 @@ console.log(type); // 'zip'
27
27
 
28
28
  ### `isZip(input)`, `isRar(input)`, `isTar(input)`, `isAsar(input)`, `is7z(input)`, `isAce(input)`
29
29
 
30
- Convenience predicates that detect an archive and return whether it matches the named format. `isAce` detects ACE (`.cba`) archives, but every actual ACE operation (read, write, or conversion) throws `UnsupportedOperationError` — see [`UnsupportedOperationError`](#unsupportedoperationerror).
30
+ Convenience predicates that detect an archive and return whether it matches the named format. `isAce` detects ACE (`.cba`) archives, but every actual ACE operation (read, write, or conversion) throws `UnsupportedOperationError`. See [`UnsupportedOperationError`](#unsupportedoperationerror).
31
31
 
32
32
  **Options**
33
33
 
@@ -58,6 +58,7 @@ Converts an archive to `zip`, `rar`, `tar`, `asar`, or `7z`. Entries are copied
58
58
  - `options.image?: { format: ImageOutputFormat; options?: ImageConvertOptions }` - Re-encode image entries while converting the archive.
59
59
  - `options.image.format` - `'webp'`, `'jpg'`, or `'png'`.
60
60
  - `options.image.options` - Image format options described under [`convertImageBuffer`](#convertimagebuffer).
61
+ - `options.metadata?: Partial<Record<MetadataSchema, ComicMetadata>>` - Replaces the source's embedded comic metadata. The source's `ComicInfo.xml`/`MetronInfo.xml` entries are dropped (an ASAR source's header metadata is never copied), then each given schema is written as an ASAR header key when the target is `asar`, or as a root-level `ComicInfo.xml`/`MetronInfo.xml` entry otherwise. Without it, an ASAR source's header metadata is not carried into the output.
61
62
 
62
63
  **Example**
63
64
 
@@ -68,6 +69,10 @@ const output = await convertArchive('/books/example.cbr', 'zip', {
68
69
  output: '/books/example.cbz',
69
70
  image: { format: 'webp', options: { webp: { quality: 92 } } },
70
71
  });
72
+
73
+ // Move a ComicInfo.xml entry into the ASAR header
74
+ const { metadata } = await readArchiveMetadata('/books/example.cbz', 'ComicInfo');
75
+ await convertArchive('/books/example.cbz', 'asar', { output: '/books/example.cbas', metadata: { ComicInfo: metadata } });
71
76
  ```
72
77
 
73
78
  ### `listArchiveFiles(input)`
@@ -102,7 +107,7 @@ const page = await cah.readArchiveEntry(comicBuffer, 'P00001.jpg');
102
107
 
103
108
  ### `readArchiveEntries(input)`
104
109
 
105
- Reads every entry's contents and SHA256 in a single pass over the archive, yielding `{ path, size?, buffer, sha256 }` in archive iteration order. Prefer this over calling `readArchiveEntry`/`sha256ArchiveEntry` once per entry: zip and asar support real random access, but the sequential/CLI-driven formats (rar, 7z, ace, tar) re-scan the archive from the start on every such call — reading every entry that way costs O(n^2) instead of O(n) for those formats. It also avoids reading each entry's data twice (once for the hash, once for the buffer).
110
+ Reads every entry's contents and SHA256 in a single pass over the archive, yielding `{ path, size?, buffer, sha256 }` in archive iteration order. Prefer this over calling `readArchiveEntry`/`sha256ArchiveEntry` once per entry: zip and asar support real random access, but the sequential/CLI-driven formats (rar, 7z, ace, tar) re-scan the archive from the start on every such call: reading every entry that way costs O(n^2) instead of O(n) for those formats. It also avoids reading each entry's data twice (once for the hash, once for the buffer).
106
111
 
107
112
  **Options**
108
113
 
@@ -191,7 +196,7 @@ const trimmed = await cah.removeArchiveEntry(comicBuffer, 'thumbs.db');
191
196
 
192
197
  ## Metadata
193
198
 
194
- `ComicMetadata` is the canonical metadata shape, covering every field defined by both bundled schemas (`schemas/ComicInfo v2.1.xsd` and `schemas/MetronInfo v1.1.xsd`) — see [`schemaVersions.ts`](../src/metadata/schemaVersions.ts) if those XSDs are ever upgraded. Conversion between ComicInfo.xml and MetronInfo.xml is intentionally lossy when a field has no equivalent in the target schema; the complete field mapping is maintained in `src/metadata/schema.ts`.
199
+ `ComicMetadata` is the canonical metadata shape, covering every field defined by both bundled schemas (`schemas/ComicInfo v2.1.xsd` and `schemas/MetronInfo v1.1.xsd`). See [`schemaVersions.ts`](../src/metadata/schemaVersions.ts) if those XSDs are ever upgraded. Conversion between ComicInfo.xml and MetronInfo.xml is intentionally lossy when a field has no equivalent in the target schema; the complete field mapping is maintained in `src/metadata/schema.ts`.
195
200
 
196
201
  List fields that MetronInfo represents as an element with an optional `id` attribute (`genres`, `tags`, `characters`, `teams`, `locations`, `stories`, `reprints`) accept either a plain string or a `{ name, id? }` object (see [`MetronResource`](#types)); the `id` is preserved on round-trip through MetronInfo.xml and dropped when writing ComicInfo.xml, which has no equivalent concept.
197
202
 
@@ -283,7 +288,7 @@ const metadata = cah.xmlToMetadata(xml, 'ComicInfo');
283
288
 
284
289
  ### `validateMetadataXml(xml, schema)`
285
290
 
286
- Validates an XML document against the bundled XSD for `'ComicInfo'` or `'MetronInfo'`, using [`xmllint-wasm`](https://www.npmjs.com/package/xmllint-wasm) (a WASM build of libxml2 — no native or Java dependency). Returns `{ valid, issues }`; `issues` is empty when the document conforms, otherwise it has one entry per schema violation with the offending element/attribute in `message` and, where available, a `line` number.
291
+ Validates an XML document against the bundled XSD for `'ComicInfo'` or `'MetronInfo'`, using [`xmllint-wasm`](https://www.npmjs.com/package/xmllint-wasm) (a WASM build of libxml2; no native or Java dependency). Returns `{ valid, issues }`; `issues` is empty when the document conforms, otherwise it has one entry per schema violation with the offending element/attribute in `message` and, where available, a `line` number.
287
292
 
288
293
  Because the validator implements XSD 1.0, it cannot check the two `<xs:assert>` business rules in MetronInfo.xml v1.1 (at most one primary `URL`, at most one primary `ID`); everything else in both schemas is enforced.
289
294
 
@@ -306,9 +311,21 @@ if (!valid) {
306
311
  }
307
312
  ```
308
313
 
314
+ **ASAR is the one exception to everything below**: an asar archive never
315
+ stores metadata as a `ComicInfo.xml`/`MetronInfo.xml` entry. Instead,
316
+ `hasComicMetadata`/`readArchiveMetadata`/`addMetadataToArchive`/
317
+ `removeComicMetadata` all read/write a `comicMetadata` key embedded directly
318
+ in the asar container's own header (the small JSON block at the start of
319
+ every `.asar` file, alongside the header's existing `files` key), as plain
320
+ JSON; no XML involved. This is transparent to callers: the same four
321
+ functions work identically regardless of archive type, and `schema` still
322
+ selects `'ComicInfo'` vs `'MetronInfo'` field semantics either way. A
323
+ `ComicInfo.xml`/`MetronInfo.xml` _entry_ inside an asar archive is never
324
+ read as metadata by any of these functions.
325
+
309
326
  ### `hasComicMetadata(input)`
310
327
 
311
- Checks for a root-level or nested `ComicInfo.xml` or `MetronInfo.xml` entry without parsing it. Returns `{ present, schema?, path? }`.
328
+ Checks for a root-level or nested `ComicInfo.xml` or `MetronInfo.xml` entry without parsing it (or, for asar, a `comicMetadata` header key). Returns `{ present, schema?, path?, bothPresent? }`. `path` is only set for a real archive entry (never for asar); `bothPresent` (asar only) is `true` when both `ComicInfo` and `MetronInfo` are present in the header.
312
329
 
313
330
  **Options**
314
331
 
@@ -323,13 +340,14 @@ if (result.present) {
323
340
  }
324
341
  ```
325
342
 
326
- ### `readArchiveMetadata(input)`
343
+ ### `readArchiveMetadata(input, schema?)`
327
344
 
328
- Finds and parses the first recognized metadata entry. Returns `{ schema, metadata }`, or `null` when no metadata file is present.
345
+ Finds and parses metadata. With no `schema`, returns whichever is found first (asar checks ComicInfo before MetronInfo). Pass `schema` to read that one specifically regardless of preference. The only way to read a non-preferred schema back out of an asar archive that has both, since asar metadata isn't addressable by path. Returns `{ schema, metadata }`, or `null` when that metadata isn't present.
329
346
 
330
347
  **Options**
331
348
 
332
349
  - `input: ArchiveInput` - A filesystem path or archive `Buffer`.
350
+ - `schema?: 'ComicInfo' | 'MetronInfo'` - Read this specific schema instead of whichever is preferred.
333
351
 
334
352
  **Example**
335
353
 
@@ -340,7 +358,7 @@ console.log(result?.metadata.title);
340
358
 
341
359
  ### `addMetadataToArchive(input, metadata, schema, options?)`
342
360
 
343
- Adds a `ComicInfo.xml` or `MetronInfo.xml` entry to an archive. Existing metadata is rejected unless `overwrite: true` is supplied.
361
+ Adds a `ComicInfo.xml` or `MetronInfo.xml` entry to an archive (or, for asar, sets the `comicMetadata.{schema}` header key). Existing metadata is rejected unless `overwrite: true` is supplied.
344
362
 
345
363
  **Options**
346
364
 
@@ -366,6 +384,23 @@ const withMetadata = await cah.addMetadataToArchive(
366
384
  );
367
385
  ```
368
386
 
387
+ ### `removeComicMetadata(input, schema, options?)`
388
+
389
+ Removes the embedded `schema` metadata (a `ComicInfo.xml`/`MetronInfo.xml` entry, or, for asar, the `comicMetadata.{schema}` header key). Throws `ArchiveFormatError` if that schema isn't present. Check `hasComicMetadata` first if you want a no-op instead.
390
+
391
+ **Options**
392
+
393
+ - `input: ArchiveInput` - A filesystem path or archive `Buffer`.
394
+ - `schema: 'ComicInfo' | 'MetronInfo'` - Metadata to remove.
395
+ - `options.tempDir?: string` - Temporary staging directory for ASAR or 7z operations.
396
+ - `options.output?: string | Writable` - Output destination. Without it, returns a `Buffer`.
397
+
398
+ **Example**
399
+
400
+ ```js
401
+ const withoutMetronInfo = await cah.removeComicMetadata(comicBuffer, 'MetronInfo');
402
+ ```
403
+
369
404
  ## Image conversion and detection
370
405
 
371
406
  ### `convertImageBuffer(image, format, options?)`
@@ -552,7 +587,7 @@ const digest = await cah.sha256Archive('/books/example.cbz');
552
587
 
553
588
  ### `benchmarkArchive(filePath, options?)`
554
589
 
555
- Extracts a comic archive into the report's `source/` subdirectory, validates it contains image files, then generates one archive per writable container format (`zip`, `tar`, `asar`, `7z`) times benchmarked page image format (`webp`, `png`, `jpg` by default, or a subset via `options.imageFormats`) — 12 variants in total by default, each containing only the original's XML and image entries, always re-encoded from the untouched source pages (never from a previously-converted variant). Benchmarks each variant's average archive-creation time (packaging only — images are re-encoded once per format before timing starts) and average random single-entry read time, writes the generated archives and a markdown report to a timestamped subdirectory, and returns the results.
590
+ Extracts a comic archive into the report's `source/` subdirectory, validates it contains image files, then generates one archive per writable container format (`zip`, `tar`, `asar`, `7z`) times benchmarked page image format (`webp`, `png`, `jpg` by default, or a subset via `options.imageFormats`). 12 variants in total by default, each containing only the original's XML and image entries, always re-encoded from the untouched source pages (never from a previously-converted variant). Benchmarks each variant's average archive-creation time (packaging only; images are re-encoded once per format before timing starts) and average random single-entry read time, writes the generated archives and a markdown report to a timestamped subdirectory, and returns the results.
556
591
 
557
592
  Throws `ArchiveFormatError` (undetectable format) or `UnsupportedOperationError` (ACE) if `filePath` isn't extractable, `NoImagesFoundError` if the archive has no image files, `FilesystemAccessError` if `filePath` doesn't exist, and `RangeError` if `options.imageFormats` is empty or names an unsupported format.
558
593
 
@@ -722,7 +757,7 @@ console.log(cah.joinResourceNames([{ name: 'Action', id: 'genre-1' }, 'Adventure
722
757
 
723
758
  ### Schema reference enums
724
759
 
725
- Readonly value lists for the enumerated (`xs:enumeration`) types in each XSD. These are reference data, not enforced constraints — fields such as `ageRating` remain plain `string` so unrecognized or future values still round-trip.
760
+ Readonly value lists for the enumerated (`xs:enumeration`) types in each XSD. These are reference data, not enforced constraints. Fields such as `ageRating` remain plain `string` so unrecognized or future values still round-trip.
726
761
 
727
762
  - `COMIC_INFO_YES_NO_VALUES` - `'Unknown' | 'No' | 'Yes'` (ComicInfo `BlackAndWhite`).
728
763
  - `COMIC_INFO_MANGA_VALUES` - `'Unknown' | 'No' | 'Yes' | 'YesAndRightToLeft'` (ComicInfo `Manga`).
@@ -731,7 +766,7 @@ Readonly value lists for the enumerated (`xs:enumeration`) types in each XSD. Th
731
766
  - `METRON_FORMAT_VALUES` - MetronInfo `Series > Format` values, e.g. `'Single Issue'`, `'Trade Paperback'`.
732
767
  - `METRON_INFORMATION_SOURCE_VALUES` - MetronInfo `IDS > ID` `source` attribute values, e.g. `'Comic Vine'`, `'Metron'`.
733
768
  - `METRON_ROLE_VALUES` - MetronInfo `Credits > Credit > Roles > Role` values (a larger set than `COMIC_INFO_CREDIT_ROLES`).
734
- - `METRON_AGE_RATING_VALUES` - MetronInfo `AgeRating` values — a different set from ComicInfo's.
769
+ - `METRON_AGE_RATING_VALUES` - MetronInfo `AgeRating` values; a different set from ComicInfo's.
735
770
 
736
771
  **Example**
737
772
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clearmist-labs/comic-archive-handler",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Detect, convert, and manipulate comic book archives: container conversion, edit metadata, image re-encoding, perceptual + content hashing.",
5
5
  "keywords": [
6
6
  "7z",