@clearmist-labs/comic-archive-handler 1.1.0 → 1.2.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/dist/index.d.mts CHANGED
@@ -480,12 +480,13 @@ export declare function sha256Archive(input: ArchiveInput): Promise<string>;
480
480
  * ComicInfo.xml/MetronInfo.xml) untouched.
481
481
  *
482
482
  * This requires two passes over the archive: `listEntries` is called once to
483
- * collect image paths (metadata only — no content is read, so no
483
+ * collect image paths (metadata only, no content is read, so no
484
484
  * decompression work is wasted) to compute the sort-order rename map, then
485
- * called again to stream entries out under their new names. Streaming
486
- * adapters (zip in particular) can't be "rewound" mid-read, so the second
487
- * pass re-invokes `listEntries` from the start rather than reusing entry
488
- * objects collected in the first pass.
485
+ * called again to stream entries out under their new names. `listEntries` is
486
+ * re-invoked from the start for the second pass rather than reusing entry
487
+ * objects collected in the first, since the sequential/CLI-driven adapters
488
+ * (rar, 7z, ace, tar) can't be "rewound" mid-read even though zip and asar
489
+ * could serve both passes from the entries collected the first time round.
489
490
  */
490
491
  export declare function renameArchiveImagesSequentially(input: ArchiveInput, options?: RenameOptions): Promise<Buffer | void>;
491
492
  //#endregion
@@ -498,6 +499,26 @@ export declare function listArchiveFiles(input: ArchiveInput): Promise<string[]>
498
499
  //#region src/readArchiveEntry.d.ts
499
500
  export declare function readArchiveEntry(input: ArchiveInput, entryPath: string): Promise<Buffer>;
500
501
  //#endregion
502
+ //#region src/readArchiveEntries.d.ts
503
+ export interface ReadArchiveEntriesResult {
504
+ path: string;
505
+ size?: number;
506
+ buffer: Buffer;
507
+ sha256: string;
508
+ }
509
+ /**
510
+ * Reads every entry's bytes and SHA256 in a single pass over the archive.
511
+ * Calling `readArchiveEntry`/`sha256ArchiveEntry` once per entry instead
512
+ * means re-running `listEntries` and re-locating the target entry on every
513
+ * call — cheap for zip (backed by real central-directory random access) and
514
+ * tar/asar, but for the sequential/CLI-driven formats (rar, 7z, ace) each
515
+ * call re-reads the archive from the start, so reading every entry that way
516
+ * costs O(n^2). This also does half the I/O of calling both
517
+ * `sha256ArchiveEntry` and `readArchiveEntry` per entry, since it computes
518
+ * the hash and buffer from the same read instead of two.
519
+ */
520
+ export declare function readArchiveEntries(input: ArchiveInput): AsyncGenerator<ReadArchiveEntriesResult>;
521
+ //#endregion
501
522
  //#region src/benchmark/benchmarkArchive.d.ts
502
523
  /**
503
524
  * Extracts a comic archive, validates it contains image files, then
@@ -537,6 +558,7 @@ declare const comicArchiveHandler: {
537
558
  stripNonEssentialFiles: typeof stripNonEssentialFiles;
538
559
  listArchiveFiles: typeof listArchiveFiles;
539
560
  readArchiveEntry: typeof readArchiveEntry;
561
+ readArchiveEntries: typeof readArchiveEntries;
540
562
  benchmarkArchive: typeof benchmarkArchive;
541
563
  renderBenchmarkReportMarkdown: typeof renderBenchmarkReportMarkdown;
542
564
  WRITABLE_ARCHIVE_TYPES: readonly WritableArchiveType[];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/benchmark/types.ts","../src/errors.ts","../src/detect.ts","../src/convertArchive.ts","../src/extractArchive.ts","../src/metadata/comicInfo.ts","../src/metadata/metronInfo.ts","../src/metadata/validate.ts","../src/metadata/schema.ts","../src/metadata/index.ts","../src/images/convert.ts","../src/images/phash.ts","../src/images/isImage.ts","../src/hashing/sha256.ts","../src/rename.ts","../src/strip.ts","../src/listFiles.ts","../src/readArchiveEntry.ts","../src/benchmark/benchmarkArchive.ts","../src/benchmark/report.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/benchmark/types.ts","../src/errors.ts","../src/detect.ts","../src/convertArchive.ts","../src/extractArchive.ts","../src/metadata/comicInfo.ts","../src/metadata/metronInfo.ts","../src/metadata/validate.ts","../src/metadata/schema.ts","../src/metadata/index.ts","../src/images/convert.ts","../src/images/phash.ts","../src/images/isImage.ts","../src/hashing/sha256.ts","../src/rename.ts","../src/strip.ts","../src/listFiles.ts","../src/readArchiveEntry.ts","../src/readArchiveEntries.ts","../src/benchmark/benchmarkArchive.ts","../src/benchmark/report.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import * as fs from "node:fs";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import * as fsp from "node:fs/promises";
6
6
  import { PassThrough, Readable } from "node:stream";
7
- import { Unzip, UnzipInflate, Zip, ZipDeflate } from "fflate";
7
+ import { Zip, ZipDeflate, inflateSync } from "fflate";
8
8
  import * as tarStream from "tar-stream";
9
9
  import { createExtractorFromData } from "node-unrar-js";
10
10
  import * as path from "node:path";
@@ -199,86 +199,163 @@ async function isAce(input) {
199
199
  return await detectArchiveType(input) === "ace";
200
200
  }
201
201
  //#endregion
202
- //#region src/internal/streamUtils.ts
203
- async function streamToBuffer(stream) {
204
- const chunks = [];
205
- for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
206
- return Buffer.concat(chunks);
202
+ //#region src/internal/zipCentralDirectory.ts
203
+ const EOCD_SIGNATURE = 101010256;
204
+ const ZIP64_EOCD_LOCATOR_SIGNATURE = 117853008;
205
+ const ZIP64_EOCD_SIGNATURE = 101075792;
206
+ const CENTRAL_DIRECTORY_SIGNATURE = 33639248;
207
+ const LOCAL_FILE_HEADER_SIGNATURE = 67324752;
208
+ const ZIP64_EXTRA_FIELD_ID = 1;
209
+ const EOCD_FIXED_SIZE = 22;
210
+ const EOCD_MAX_COMMENT_SIZE = 65535;
211
+ const ZIP64_EOCD_LOCATOR_SIZE = 20;
212
+ const UINT32_MAX = 4294967295;
213
+ const UINT16_MAX = 65535;
214
+ /**
215
+ * The end-of-central-directory record sits at the end of the file, after an
216
+ * optional variable-length comment, so it's found by scanning backward from
217
+ * the end for its signature rather than reading forward from the start.
218
+ */
219
+ async function findEndOfCentralDirectory(input) {
220
+ const size = await inputSize(input);
221
+ const searchStart = Math.max(0, size - EOCD_FIXED_SIZE - EOCD_MAX_COMMENT_SIZE);
222
+ const tail = await readInputRange(input, searchStart, size);
223
+ for (let i = tail.length - EOCD_FIXED_SIZE; i >= 0; i--) if (tail.readUInt32LE(i) === EOCD_SIGNATURE) return {
224
+ record: tail.subarray(i),
225
+ offset: searchStart + i
226
+ };
227
+ throw new ArchiveFormatError("Not a valid zip archive: no end-of-central-directory record found.");
207
228
  }
208
- /** Bridges a callback-driven push source into an async iterable, in arrival order. */
209
- var AsyncQueue = class {
210
- items = [];
211
- waiter = null;
212
- done = false;
213
- error = null;
214
- push(item) {
215
- this.items.push(item);
216
- this.waiter?.();
217
- }
218
- finish() {
219
- this.done = true;
220
- this.waiter?.();
221
- }
222
- fail(err) {
223
- this.error = err;
224
- this.done = true;
225
- this.waiter?.();
226
- }
227
- async *[Symbol.asyncIterator]() {
228
- for (;;) {
229
- if (this.items.length) {
230
- yield this.items.shift();
231
- continue;
229
+ /**
230
+ * Zip64 stores the real (64-bit) totals in its own record, pointed to by a
231
+ * fixed-size locator that sits immediately before the regular
232
+ * end-of-central-directory record.
233
+ */
234
+ async function readZip64EndOfCentralDirectory(input, eocdOffset) {
235
+ const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_SIZE;
236
+ if (locatorOffset < 0) throw new ArchiveFormatError("Not a valid zip64 archive: missing end-of-central-directory locator.");
237
+ const locator = await readInputRange(input, locatorOffset, locatorOffset + ZIP64_EOCD_LOCATOR_SIZE);
238
+ if (locator.length < ZIP64_EOCD_LOCATOR_SIZE || locator.readUInt32LE(0) !== ZIP64_EOCD_LOCATOR_SIGNATURE) throw new ArchiveFormatError("Not a valid zip64 archive: missing end-of-central-directory locator.");
239
+ const zip64EocdOffset = Number(locator.readBigUInt64LE(8));
240
+ const record = await readInputRange(input, zip64EocdOffset, zip64EocdOffset + 56);
241
+ if (record.length < 56 || record.readUInt32LE(0) !== ZIP64_EOCD_SIGNATURE) throw new ArchiveFormatError("Not a valid zip64 archive: malformed end-of-central-directory record.");
242
+ return {
243
+ totalEntries: Number(record.readBigUInt64LE(32)),
244
+ centralDirectorySize: Number(record.readBigUInt64LE(40)),
245
+ centralDirectoryOffset: Number(record.readBigUInt64LE(48))
246
+ };
247
+ }
248
+ /** Reads the zip64 extra field's 8-byte values for whichever fixed-header fields overflowed 32 bits, in the fixed order the spec requires: uncompressed size, compressed size, local header offset. */
249
+ function readZip64ExtraField(extra, overflowed) {
250
+ let pos = 0;
251
+ while (pos + 4 <= extra.length) {
252
+ const fieldId = extra.readUInt16LE(pos);
253
+ const fieldSize = extra.readUInt16LE(pos + 2);
254
+ if (fieldId === ZIP64_EXTRA_FIELD_ID) {
255
+ let valuePos = pos + 4;
256
+ const result = {};
257
+ if (overflowed.uncompressedSize && valuePos + 8 <= extra.length) {
258
+ result.uncompressedSize = Number(extra.readBigUInt64LE(valuePos));
259
+ valuePos += 8;
232
260
  }
233
- if (this.error) throw this.error;
234
- if (this.done) return;
235
- await new Promise((resolve) => {
236
- this.waiter = resolve;
237
- });
238
- this.waiter = null;
261
+ if (overflowed.compressedSize && valuePos + 8 <= extra.length) {
262
+ result.compressedSize = Number(extra.readBigUInt64LE(valuePos));
263
+ valuePos += 8;
264
+ }
265
+ if (overflowed.localHeaderOffset && valuePos + 8 <= extra.length) {
266
+ result.localHeaderOffset = Number(extra.readBigUInt64LE(valuePos));
267
+ valuePos += 8;
268
+ }
269
+ return result;
239
270
  }
271
+ pos += 4 + fieldSize;
240
272
  }
241
- };
242
- //#endregion
243
- //#region src/archive/zip.ts
244
- function toEntry(file) {
245
- return {
246
- path: file.name,
247
- size: file.originalSize,
248
- openReadStream() {
249
- const pass = new PassThrough();
250
- file.ondata = (err, data, final) => {
251
- if (err) {
252
- pass.destroy(err);
253
- return;
254
- }
255
- if (data.length) pass.write(Buffer.from(data));
256
- if (final) pass.end();
257
- };
258
- file.start();
259
- return pass;
273
+ throw new ArchiveFormatError("Not a valid zip64 archive: an oversized entry is missing its zip64 extra field.");
274
+ }
275
+ /**
276
+ * Parses a zip's central directory — the entry index at the end of the
277
+ * file — into per-entry metadata (name, sizes, compression method, and the
278
+ * offset of its local file header). Reading this index costs a couple of
279
+ * small range reads regardless of archive size; it never reads or
280
+ * decompresses entry data itself, which is why entries can then be read in
281
+ * any order at O(1) cost each rather than needing a forward scan through
282
+ * every entry that precedes the one wanted.
283
+ */
284
+ async function parseZipCentralDirectory(input) {
285
+ const { record: eocd, offset: eocdOffset } = await findEndOfCentralDirectory(input);
286
+ let totalEntries = eocd.readUInt16LE(10);
287
+ let centralDirectorySize = eocd.readUInt32LE(12);
288
+ let centralDirectoryOffset = eocd.readUInt32LE(16);
289
+ if (totalEntries === UINT16_MAX || centralDirectorySize === UINT32_MAX || centralDirectoryOffset === UINT32_MAX) ({totalEntries, centralDirectorySize, centralDirectoryOffset} = await readZip64EndOfCentralDirectory(input, eocdOffset));
290
+ const centralDirectory = await readInputRange(input, centralDirectoryOffset, centralDirectoryOffset + centralDirectorySize);
291
+ const entries = [];
292
+ let pos = 0;
293
+ for (let i = 0; i < totalEntries; i++) {
294
+ if (pos + 46 > centralDirectory.length || centralDirectory.readUInt32LE(pos) !== CENTRAL_DIRECTORY_SIGNATURE) throw new ArchiveFormatError("Not a valid zip archive: malformed central directory entry.");
295
+ const compressionMethod = centralDirectory.readUInt16LE(pos + 10);
296
+ let compressedSize = centralDirectory.readUInt32LE(pos + 20);
297
+ let uncompressedSize = centralDirectory.readUInt32LE(pos + 24);
298
+ const nameLength = centralDirectory.readUInt16LE(pos + 28);
299
+ const extraLength = centralDirectory.readUInt16LE(pos + 30);
300
+ const commentLength = centralDirectory.readUInt16LE(pos + 32);
301
+ let localHeaderOffset = centralDirectory.readUInt32LE(pos + 42);
302
+ const name = centralDirectory.toString("utf8", pos + 46, pos + 46 + nameLength);
303
+ const overflowed = {
304
+ uncompressedSize: uncompressedSize === UINT32_MAX,
305
+ compressedSize: compressedSize === UINT32_MAX,
306
+ localHeaderOffset: localHeaderOffset === UINT32_MAX
307
+ };
308
+ if (overflowed.uncompressedSize || overflowed.compressedSize || overflowed.localHeaderOffset) {
309
+ const zip64Values = readZip64ExtraField(centralDirectory.subarray(pos + 46 + nameLength, pos + 46 + nameLength + extraLength), overflowed);
310
+ uncompressedSize = zip64Values.uncompressedSize ?? uncompressedSize;
311
+ compressedSize = zip64Values.compressedSize ?? compressedSize;
312
+ localHeaderOffset = zip64Values.localHeaderOffset ?? localHeaderOffset;
260
313
  }
261
- };
314
+ if (!name.endsWith("/")) entries.push({
315
+ path: name,
316
+ compressedSize,
317
+ uncompressedSize,
318
+ compressionMethod,
319
+ localHeaderOffset
320
+ });
321
+ pos += 46 + nameLength + extraLength + commentLength;
322
+ }
323
+ return entries;
262
324
  }
325
+ /**
326
+ * Reads and decompresses one entry's data, using its central directory
327
+ * metadata to locate the bytes directly — no scan through other entries.
328
+ * The local file header still has to be read first because its name/extra
329
+ * field lengths (which can differ from the central directory's) are what
330
+ * determine where the entry's actual data starts.
331
+ */
332
+ async function readZipEntryData(input, entry) {
333
+ const localHeader = await readInputRange(input, entry.localHeaderOffset, entry.localHeaderOffset + 30);
334
+ if (localHeader.length < 30 || localHeader.readUInt32LE(0) !== LOCAL_FILE_HEADER_SIGNATURE) throw new ArchiveFormatError(`Not a valid zip archive: malformed local file header for "${entry.path}".`);
335
+ const nameLength = localHeader.readUInt16LE(26);
336
+ const extraLength = localHeader.readUInt16LE(28);
337
+ const dataStart = entry.localHeaderOffset + 30 + nameLength + extraLength;
338
+ const compressed = await readInputRange(input, dataStart, dataStart + entry.compressedSize);
339
+ if (entry.compressionMethod === 0) return compressed;
340
+ if (entry.compressionMethod === 8) return Buffer.from(inflateSync(compressed));
341
+ throw new ArchiveFormatError(`Unsupported zip compression method (${entry.compressionMethod}) for "${entry.path}".`);
342
+ }
343
+ //#endregion
344
+ //#region src/archive/zip.ts
263
345
  const zipAdapter = {
264
346
  type: "zip",
265
347
  canWrite: true,
266
348
  async *listEntries(input) {
267
- const source = openInputReadStream(input);
268
- const queue = new AsyncQueue();
269
- const unzip = new Unzip();
270
- unzip.register(UnzipInflate);
271
- unzip.onfile = (file) => {
272
- if (file.name.endsWith("/")) return;
273
- queue.push(toEntry(file));
349
+ const entries = await parseZipCentralDirectory(input);
350
+ for (const entry of entries) yield {
351
+ path: entry.path,
352
+ size: entry.uncompressedSize,
353
+ openReadStream() {
354
+ return Readable.from((async function* () {
355
+ yield await readZipEntryData(input, entry);
356
+ })());
357
+ }
274
358
  };
275
- source.on("data", (chunk) => unzip.push(new Uint8Array(chunk), false));
276
- source.on("end", () => {
277
- unzip.push(/* @__PURE__ */ new Uint8Array(0), true);
278
- queue.finish();
279
- });
280
- source.on("error", (err) => queue.fail(err));
281
- yield* queue;
282
359
  },
283
360
  async write(entries, destination) {
284
361
  await new Promise((resolve, reject) => {
@@ -307,6 +384,13 @@ const zipAdapter = {
307
384
  }
308
385
  };
309
386
  //#endregion
387
+ //#region src/internal/streamUtils.ts
388
+ async function streamToBuffer(stream) {
389
+ const chunks = [];
390
+ for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
391
+ return Buffer.concat(chunks);
392
+ }
393
+ //#endregion
310
394
  //#region src/archive/tar.ts
311
395
  /**
312
396
  * The tar-stream v3 package is built on `streamx`, not Node's native streams;
@@ -1844,12 +1928,13 @@ function naturalCompare(a, b) {
1844
1928
  * ComicInfo.xml/MetronInfo.xml) untouched.
1845
1929
  *
1846
1930
  * This requires two passes over the archive: `listEntries` is called once to
1847
- * collect image paths (metadata only — no content is read, so no
1931
+ * collect image paths (metadata only, no content is read, so no
1848
1932
  * decompression work is wasted) to compute the sort-order rename map, then
1849
- * called again to stream entries out under their new names. Streaming
1850
- * adapters (zip in particular) can't be "rewound" mid-read, so the second
1851
- * pass re-invokes `listEntries` from the start rather than reusing entry
1852
- * objects collected in the first pass.
1933
+ * called again to stream entries out under their new names. `listEntries` is
1934
+ * re-invoked from the start for the second pass rather than reusing entry
1935
+ * objects collected in the first, since the sequential/CLI-driven adapters
1936
+ * (rar, 7z, ace, tar) can't be "rewound" mid-read even though zip and asar
1937
+ * could serve both passes from the entries collected the first time round.
1853
1938
  */
1854
1939
  async function renameArchiveImagesSequentially(input, options = {}) {
1855
1940
  const adapter = getAdapter(await detectArchiveType(input));
@@ -1909,6 +1994,37 @@ async function readArchiveEntry(input, entryPath) {
1909
1994
  throw new MetadataNotFoundError(`No entry named "${entryPath}" was found in the archive.`);
1910
1995
  }
1911
1996
  //#endregion
1997
+ //#region src/readArchiveEntries.ts
1998
+ /**
1999
+ * Reads every entry's bytes and SHA256 in a single pass over the archive.
2000
+ * Calling `readArchiveEntry`/`sha256ArchiveEntry` once per entry instead
2001
+ * means re-running `listEntries` and re-locating the target entry on every
2002
+ * call — cheap for zip (backed by real central-directory random access) and
2003
+ * tar/asar, but for the sequential/CLI-driven formats (rar, 7z, ace) each
2004
+ * call re-reads the archive from the start, so reading every entry that way
2005
+ * costs O(n^2). This also does half the I/O of calling both
2006
+ * `sha256ArchiveEntry` and `readArchiveEntry` per entry, since it computes
2007
+ * the hash and buffer from the same read instead of two.
2008
+ */
2009
+ async function* readArchiveEntries(input) {
2010
+ const adapter = getAdapter(await detectArchiveType(input));
2011
+ for await (const entry of adapter.listEntries(input)) {
2012
+ const hash = createHash("sha256");
2013
+ const chunks = [];
2014
+ for await (const chunk of entry.openReadStream()) {
2015
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2016
+ chunks.push(buffer);
2017
+ hash.update(buffer);
2018
+ }
2019
+ yield {
2020
+ path: entry.path,
2021
+ size: entry.size,
2022
+ buffer: Buffer.concat(chunks),
2023
+ sha256: hash.digest("hex")
2024
+ };
2025
+ }
2026
+ }
2027
+ //#endregion
1912
2028
  //#region src/benchmark/timing.ts
1913
2029
  /** Runs `run` `iterations` times and returns the average duration in milliseconds. */
1914
2030
  async function averageDuration(iterations, run) {
@@ -2153,6 +2269,7 @@ const comicArchiveHandler = {
2153
2269
  stripNonEssentialFiles,
2154
2270
  listArchiveFiles,
2155
2271
  readArchiveEntry,
2272
+ readArchiveEntries,
2156
2273
  benchmarkArchive,
2157
2274
  renderBenchmarkReportMarkdown,
2158
2275
  WRITABLE_ARCHIVE_TYPES,
@@ -2160,6 +2277,6 @@ const comicArchiveHandler = {
2160
2277
  ARCHIVE_TYPE_EXTENSIONS
2161
2278
  };
2162
2279
  //#endregion
2163
- export { ARCHIVE_TYPE_EXTENSIONS, ArchiveFormatError, BENCHMARK_IMAGE_FORMATS, COMIC_INFO_AGE_RATING_VALUES, COMIC_INFO_CREDIT_ROLES, COMIC_INFO_MANGA_VALUES, COMIC_INFO_PAGE_TYPE_VALUES, COMIC_INFO_YES_NO_VALUES, FilesystemAccessError, IMAGE_EXTENSIONS, METRON_AGE_RATING_VALUES, METRON_FORMAT_VALUES, METRON_INFORMATION_SOURCE_VALUES, METRON_ROLE_VALUES, MetadataNotFoundError, NoImagesFoundError, SevenZipUnavailableError, UnsupportedOperationError, WRITABLE_ARCHIVE_TYPES, addMetadataToArchive, benchmarkArchive, comicInfoXmlToMetadata, computeArchiveImagePHash, computeImagePHash, convertArchive, convertArchiveImages, convertImageBuffer, comicArchiveHandler as default, detectArchiveType, extractArchive, getExtension, hammingDistance, hasComicMetadata, is7z, isAce, isAsar, isImagePath, isRar, isTar, isZip, joinCommaList, joinResourceNames, listArchiveFiles, metadataToComicInfoXml, metadataToMetronInfoXml, metadataToXml, metronInfoXmlToMetadata, phashToHex, readArchiveEntry, readArchiveMetadata, renameArchiveImagesSequentially, renderBenchmarkReportMarkdown, resourceId, resourceName, sha256Archive, sha256ArchiveEntry, splitCommaList, stripNonEssentialFiles, validateMetadataXml, xmlToMetadata };
2280
+ export { ARCHIVE_TYPE_EXTENSIONS, ArchiveFormatError, BENCHMARK_IMAGE_FORMATS, COMIC_INFO_AGE_RATING_VALUES, COMIC_INFO_CREDIT_ROLES, COMIC_INFO_MANGA_VALUES, COMIC_INFO_PAGE_TYPE_VALUES, COMIC_INFO_YES_NO_VALUES, FilesystemAccessError, IMAGE_EXTENSIONS, METRON_AGE_RATING_VALUES, METRON_FORMAT_VALUES, METRON_INFORMATION_SOURCE_VALUES, METRON_ROLE_VALUES, MetadataNotFoundError, NoImagesFoundError, SevenZipUnavailableError, UnsupportedOperationError, WRITABLE_ARCHIVE_TYPES, addMetadataToArchive, benchmarkArchive, comicInfoXmlToMetadata, computeArchiveImagePHash, computeImagePHash, convertArchive, convertArchiveImages, convertImageBuffer, comicArchiveHandler as default, detectArchiveType, extractArchive, getExtension, hammingDistance, hasComicMetadata, is7z, isAce, isAsar, isImagePath, isRar, isTar, isZip, joinCommaList, joinResourceNames, listArchiveFiles, metadataToComicInfoXml, metadataToMetronInfoXml, metadataToXml, metronInfoXmlToMetadata, phashToHex, readArchiveEntries, readArchiveEntry, readArchiveMetadata, renameArchiveImagesSequentially, renderBenchmarkReportMarkdown, resourceId, resourceName, sha256Archive, sha256ArchiveEntry, splitCommaList, stripNonEssentialFiles, validateMetadataXml, xmlToMetadata };
2164
2281
 
2165
2282
  //# sourceMappingURL=index.mjs.map