Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5e5678d
Initial commit - cache confusion
omega-fallon Mar 22, 2026
490e8d6
Error reporting for failed handler initialization
omega-fallon Mar 22, 2026
98ac7c3
Functional except for cba<->cba
omega-fallon Mar 23, 2026
c06841d
Remove testing message, whoops
omega-fallon Mar 23, 2026
5d8908a
Weight adjustment to make archive to archive always be preferred, if …
omega-fallon Mar 23, 2026
cefd7f8
Proper output name for archive and files inside
omega-fallon Mar 23, 2026
c76fd84
Even proper-er naming.
omega-fallon Mar 23, 2026
2e01b55
Reducing redundancy between comics handler and sevenZip handler
omega-fallon Mar 23, 2026
3b18570
Spacing and comments
omega-fallon Mar 23, 2026
e0aba83
Removing now-unused list
omega-fallon Mar 23, 2026
89f17b2
More sensible extension-checking logic
omega-fallon Mar 23, 2026
66c1a7d
Ensuring only CBX-valid formats get packed into CBXs
omega-fallon Mar 23, 2026
56e9c00
Prevent non-CBX to CBX direct conversions
omega-fallon Mar 23, 2026
58d4547
Fixed input format checking for CBX archives and added new category a…
omega-fallon Mar 25, 2026
6bd43b5
Proper weights and test validation
omega-fallon Mar 25, 2026
42b5e64
Fixed issue where sevenZip converter could silently output invalid files
omega-fallon Mar 25, 2026
8854217
Removing redundant weight
omega-fallon Mar 25, 2026
0a28a6b
rezipping prevention
omega-fallon Apr 6, 2026
24744e5
Checking for null category
omega-fallon Apr 6, 2026
08fce34
Merge branch 'master' into comic-books-2
omega-fallon Jun 19, 2026
2c69eda
Merge branch 'master' into comic-books-2
omega-fallon Jul 11, 2026
b74ff0d
Including new handlers
omega-fallon Jul 11, 2026
65d1075
The innocuous missing comma
omega-fallon Jul 11, 2026
8aa352e
ZipPacker only needs to pack CBZs, leave standard packing to another …
omega-fallon Jul 11, 2026
2ba015d
Correcting weight
omega-fallon Jul 11, 2026
133e841
...Okay, I guess we do have to support this since apparently no other…
omega-fallon Jul 11, 2026
f0c3da1
Naming fix
omega-fallon Jul 11, 2026
5613bee
Correct name and weights.
omega-fallon Jul 12, 2026
5cc4be3
Finally correct zipping behavior.
omega-fallon Jul 12, 2026
c5d9877
Stupid
omega-fallon Jul 12, 2026
ebf8e5c
Padding for file names
omega-fallon Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/CommonFormats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const Category = {
TEXT: "text",
AUDIO: "audio",
ARCHIVE: "archive",
IMAGE_ARCHIVE: "image archive",
SPREADSHEET: "spreadsheet",
PRESENTATION: "presentation",
FONT: "font",
Expand Down Expand Up @@ -208,6 +209,20 @@ const CommonFormats = {
"application/x-tar",
Category.ARCHIVE
),
RAR: new FormatDefinition(
"Rar Archive",
"rar",
"rar",
"application/vnd.rar",
Category.ARCHIVE
),
SZ: new FormatDefinition(
"7z Archive",
"7z",
"7z",
"application/x-7z-compressed",
Category.ARCHIVE
),
// documents
PDF: new FormatDefinition(
"Portable Document Format",
Expand Down
5 changes: 5 additions & 0 deletions src/TraversionGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export class TraversionGraph {
{ categories: ["text", "image", "audio"], cost: 15 }, // Text to audio through an image is likely not what the user wants
{ categories: ["image", "video", "audio"], cost: 10000 }, // Converting from image to audio through video is especially lossy
{ categories: ["audio", "video", "image"], cost: 10000 }, // Converting from audio to image through video is especially lossy

{ categories: ["archive", "image", "archive"], cost: 10000 }, // If archive -> archive is possible directly, it should be prioritized pretty much always.
{ categories: ["image archive", "image", "image archive"], cost: 10000 }, // As above, but for image archives
{ categories: ["image archive", "image", "archive"], cost: 10000 }, // We should never have to do this. The reverse, however, is necessary.
{ categories: ["image", "image archive", "archive"], cost: 10000 }, // Results in unnecessary page-naming.
];
// Keeps track of path segments that have failed when attempted during the last run
private temporaryDeadEnds: ConvertPathNode[][] = [];
Expand Down
187 changes: 125 additions & 62 deletions src/handlers/comics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// file: comics.ts

import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts";
import CommonFormats from "src/CommonFormats.ts";
import CommonFormats, { Category } from "src/CommonFormats.ts";

import {
createTar,
Expand All @@ -14,56 +14,42 @@ import {
import JSZip from "jszip";

const image_list = ["png","jpg","webp","bmp","tiff","gif"];
const archives_list = ["zip","cbz","tar","cbt"];

class comicsHandler implements FormatHandler {
function padNumberString(num: number, digits: number): string {
let str = String(num);

public name: string = "comics";
while (str.length < digits) {
str = "0"+str;
}

return str;
}

export class comicsZipPackerHandler implements FormatHandler {
public name: string = "comicsZipPacker";
public supportedFormats?: FileFormat[];
public ready: boolean = false;

async init () {
this.supportedFormats = [
CommonFormats.PNG.supported("png", true, true),
CommonFormats.JPEG.supported("jpg", true, true),
CommonFormats.WEBP.supported("webp", true, true),
CommonFormats.BMP.supported("bmp", true, true),
CommonFormats.TIFF.supported("tiff", true, true),
CommonFormats.GIF.supported("gif", true, true),

CommonFormats.ZIP.supported("zip", true, true),
{
name: "Tape Archive",
format: "tar",
extension: "tar",
mime: "application/x-tar",
from: true,
to: true,
internal: "tar",
category: ["archive"],
},
CommonFormats.PNG.supported("png", true, false),
CommonFormats.JPEG.supported("jpg", true, false),
CommonFormats.WEBP.supported("webp", true, false),
CommonFormats.BMP.supported("bmp", true, false),
CommonFormats.TIFF.supported("tiff", true, false),
CommonFormats.GIF.supported("gif", true, false),

CommonFormats.ZIP.supported("zip", false, true, true),
{
name: "Comic Book Archive (ZIP)",
format: "cbz",
extension: "cbz",
mime: "application/vnd.comicbook+zip",
from: true,
from: false,
to: true,
internal: "cbz",
category: ["archive"],
lossless: true,
},
{
name: "Comic Book Archive (TAR)",
format: "cbt",
extension: "cbt",
mime: "application/vnd.comicbook+tar",
from: true,
to: true,
internal: "cbt",
category: ["archive"],
lossless: true,
category: [Category.ARCHIVE,Category.IMAGE_ARCHIVE],
lossless: false,
},
];

Expand All @@ -77,23 +63,24 @@ class comicsHandler implements FormatHandler {
): Promise<FileData[]> {
const outputFiles: FileData[] = [];

// Base name for imgs -> archive
const baseName = inputFiles[0].name.replace("_0."+inputFormat.extension,"."+inputFormat.extension).split(".").slice(0, -1).join(".");

// Single-gif catching
if (inputFormat.internal === "gif" && (archives_list.includes(outputFormat.internal)) && inputFiles.length === 1) {
throw new TypeError("User probably intends for an archive of video/gif frames; abort.");
}

// Pack a zip/cbz with code copied from wad.ts
if ((image_list.includes(inputFormat.internal)) && (outputFormat.internal === "cbz" || outputFormat.internal === "zip")) {
// Single-gif catching
if (inputFormat.internal === "gif" && inputFiles.length === 1) {
throw new TypeError("User probably intends for an archive of video/gif frames; abort.");
}

// Base name for imgs -> archive
const baseName = inputFiles[0].name.replace("_0."+inputFormat.extension,"."+inputFormat.extension).split(".").slice(0, -1).join(".");

const zip = new JSZip();

// Add files to archive
const necessaryDigits = String(inputFiles.length-1).length;
let iterations = 0;
for (const file of inputFiles) {
if (outputFormat.internal === "cbz") {
zip.file("Page "+String(iterations)+"."+inputFormat.extension, file.bytes);
zip.file(padNumberString(iterations,necessaryDigits)+"."+inputFormat.extension, file.bytes);
}
else {
zip.file(file.name, file.bytes);
Expand All @@ -104,8 +91,54 @@ class comicsHandler implements FormatHandler {
const output = await zip.generateAsync({ type: "uint8array" });
outputFiles.push({ bytes: output, name: baseName + "." + outputFormat.extension });
}
else {
throw new TypeError(`Unsupported conversion path: ${inputFormat.internal} -> ${outputFormat.internal}`);
}

return outputFiles;
}
}

export class comicsZipUnpackerHandler implements FormatHandler {
public name: string = "comicsZipUnpacker";
public supportedFormats?: FileFormat[];
public ready: boolean = false;

async init () {
this.supportedFormats = [
CommonFormats.PNG.supported("png", false, true),
CommonFormats.JPEG.supported("jpg", false, true),
CommonFormats.WEBP.supported("webp", false, true),
CommonFormats.BMP.supported("bmp", false, true),
CommonFormats.TIFF.supported("tiff", false, true),
CommonFormats.GIF.supported("gif", false, true),

CommonFormats.ZIP.supported("zip", true, false),
{
name: "Comic Book Archive (ZIP)",
format: "cbz",
extension: "cbz",
mime: "application/vnd.comicbook+zip",
from: true,
to: false,
internal: "cbz",
category: [Category.ARCHIVE,Category.IMAGE_ARCHIVE],
lossless: false,
},
];

this.ready = true;
}

async doConvert (
inputFiles: FileData[],
inputFormat: FileFormat,
outputFormat: FileFormat
): Promise<FileData[]> {
const outputFiles: FileData[] = [];

// Unpack a zip/cbz with code copied from lzh.ts
else if ((inputFormat.internal === "cbz" || inputFormat.internal === "zip") && (image_list.includes(outputFormat.internal))) {
if ((inputFormat.internal === "cbz" || inputFormat.internal === "zip") && (image_list.includes(outputFormat.internal))) {
for (const file of inputFiles) {
const zip = new JSZip();
await zip.loadAsync(file.bytes);
Expand Down Expand Up @@ -135,16 +168,54 @@ class comicsHandler implements FormatHandler {
throw new Error("No applicable files to unzip found.");
}
}
// Pack a cbt with code from tar.ts
else if (image_list.includes(inputFormat.internal) && outputFormat.internal === "cbt") {
const bytes = createTar(
inputFiles.map(file => ({ name: "Page "+inputFiles.indexOf(file)+"."+inputFormat.extension, data: file.bytes })),
{},
);
outputFiles.push({ bytes: bytes, name: baseName + "." + outputFormat.extension });
else {
throw new TypeError(`Unsupported conversion path: ${inputFormat.internal} -> ${outputFormat.internal}`);
}

return outputFiles;
}
}

export class comicsTarUnpackerHandler implements FormatHandler {
public name: string = "comicsTarUnpacker";
public supportedFormats?: FileFormat[];
public ready: boolean = false;

async init () {
this.supportedFormats = [
CommonFormats.PNG.supported("png", false, true),
CommonFormats.JPEG.supported("jpg", false, true),
CommonFormats.WEBP.supported("webp", false, true),
CommonFormats.BMP.supported("bmp", false, true),
CommonFormats.TIFF.supported("tiff", false, true),
CommonFormats.GIF.supported("gif", false, true),

CommonFormats.TAR.supported("tar", true, false),
{
name: "Comic Book Archive (TAR)",
format: "cbt",
extension: "cbt",
mime: "application/vnd.comicbook+tar",
from: true,
to: false,
internal: "cbt",
category: [Category.ARCHIVE,Category.IMAGE_ARCHIVE],
lossless: false,
},
];

this.ready = true;
}

async doConvert (
inputFiles: FileData[],
inputFormat: FileFormat,
outputFormat: FileFormat
): Promise<FileData[]> {
const outputFiles: FileData[] = [];

// Unpack a tar/cbt with code from tar.ts
else if ((inputFormat.internal === "cbt" || inputFormat.internal === "tar") && image_list.includes(outputFormat.internal)) {
if ((inputFormat.internal === "cbt" || inputFormat.internal === "tar") && image_list.includes(outputFormat.internal)) {
for (const inputFile of inputFiles) {
const files = parseTar(inputFile.bytes);

Expand Down Expand Up @@ -172,18 +243,10 @@ class comicsHandler implements FormatHandler {
throw new Error("No applicable files to unpack found.");
}
}
// Renaming interchangeable formats. Note that any valid "comic book" archive can be guaranteed as a valid standard archive, but not every valid archive can be a valid comic book archive. Thus, we only allow renaming from comic book to non-comic book formats.
else if ((inputFormat.internal === "cbz" && outputFormat.internal === "zip") || (inputFormat.internal === "cbt" && outputFormat.internal === "tar")) {
for (const file of inputFiles) {
outputFiles.push({ bytes: file.bytes, name: file.name.split(".").slice(0, -1).join(".") + "." + outputFormat.extension });
}
}
else {
throw new TypeError(`Unsupported conversion path: ${inputFormat.internal} -> ${outputFormat.internal}`);
}

return outputFiles;
}
}

export default comicsHandler;
16 changes: 11 additions & 5 deletions src/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import curaniHandler from "./curani.ts";
import bunburrowsHandler from "./bunburrows.ts";
import rgbaHandler from "./rgba.ts";
import svgTraceHandler from "./svgTrace.ts";
import { renameZipHandler, renameTxtHandler, renameJsonHandler } from "./rename.ts";
import { comicsZipPackerHandler, comicsZipUnpackerHandler, comicsTarUnpackerHandler } from "./comics.ts";
import { renameZipHandler, renameRarHandler, renameTarHandler, rename7zHandler, renameTxtHandler, renameJsonHandler } from "./rename.ts";
import envelopeHandler from "./envelope.ts";
import pandocHandler from "./pandoc.ts";
import svgForeignObjectHandler from "./svgForeignObject.ts";
Expand All @@ -37,7 +38,7 @@ import jsonToCHandler from "./jsonToC.ts";
import turbowarpHandler from "./turbowarp.ts";
import libopenmptHandler from "./libopenmpt.ts";
import { midiCodecHandler, midiSynthHandler } from "./midi.ts";
import lzhHandler from "./lzh.ts";
import { LZHHandler, LZH2Handler } from "./lzh.ts";
import txtToInfiniteCraftHandler from "./txtToInfiniteCraft.ts";
import wadHandler from "./wad.ts";
import espeakngHandler from "./espeakng.js"
Expand All @@ -54,7 +55,6 @@ import vexflowHandler from "./vexflow.ts";
import toonHandler from "./toon.ts";
import rpgmvpHandler from "./rpgmvp.ts";
import otaHandler from "./ota.ts";
import comicsHandler from "./comics.ts";
import terrariaWldHandler from "./terrariawld.ts";
import { opusMagnumMainHandler, opusMagnumTTMHandler, opusMagnumITMHandler } from "./opusMagnum.ts";
import aperturePictureHandler from "./aperturePicture.ts";
Expand Down Expand Up @@ -89,7 +89,13 @@ try { handlers.push(new ImageMagickHandler()) } catch (_) { };
try { handlers.push(new curaniHandler()) } catch (_) { };
try { handlers.push(new bunburrowsHandler()) } catch (_) { };
try { handlers.push(new rgbaHandler()) } catch (_) { };
try { handlers.push(new comicsZipPackerHandler()) } catch (_) { };
try { handlers.push(new comicsZipUnpackerHandler()) } catch (_) { };
try { handlers.push(new comicsTarUnpackerHandler()) } catch (_) { };
try { handlers.push(renameZipHandler) } catch (_) { };
try { handlers.push(renameTarHandler) } catch (_) { };
try { handlers.push(renameRarHandler) } catch (_) { };
try { handlers.push(rename7zHandler) } catch (_) { };
try { handlers.push(renameTxtHandler) } catch (_) { };
try { handlers.push(renameJsonHandler) } catch (_) { };
try { handlers.push(new envelopeHandler()) } catch (_) { };
Expand Down Expand Up @@ -119,7 +125,8 @@ try { handlers.push(new jsonToCHandler()) } catch (_) { };
try { handlers.push(new libopenmptHandler()) } catch (_) { };
try { handlers.push(new midiCodecHandler()) } catch (_) { };
try { handlers.push(new midiSynthHandler()) } catch (_) { };
try { handlers.push(new lzhHandler()) } catch (_) { };
try { handlers.push(new LZHHandler()) } catch (_) { };
try { handlers.push(new LZH2Handler()) } catch (_) { };
try { handlers.push(new wadHandler()) } catch (_) { };
try { handlers.push(new pandocHandler()) } catch (_) { };
try { handlers.push(new txtToInfiniteCraftHandler()) } catch (_) { };
Expand All @@ -137,7 +144,6 @@ try { handlers.push(new vexflowHandler()) } catch (_) { };
try { handlers.push(new toonHandler()) } catch (_) { };
try { handlers.push(new rpgmvpHandler()) } catch (_) { };
try { handlers.push(new otaHandler()) } catch (_) { };
try { handlers.push(new comicsHandler()) } catch (_) { };
try { handlers.push(new terrariaWldHandler()) } catch (_) { };
try { handlers.push(new opusMagnumMainHandler()) } catch (_) { };
try { handlers.push(new opusMagnumTTMHandler()) } catch (_) { };
Expand Down
Loading
Loading