Skip to content

Commit

Permalink
feat: support bundle simple epub
Browse files Browse the repository at this point in the history
  • Loading branch information
yjl9903 committed Feb 26, 2023
1 parent c85673b commit 32e23a9
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 110 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules
# Build output
dist
.turbo
.output

# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
Expand Down
25 changes: 20 additions & 5 deletions packages/core/src/bundle/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as fflate from 'fflate';
import * as path from 'node:path';
import { XMLBuilder } from 'fast-xml-parser';

import type { Epubook, ManifestItem, ManifestItemRef, PackageDocument } from '../epub';

import { BundleError } from '../error';

import { MIMETYPE } from './constant';

export * from './constant';
import { MIMETYPE } from '../constant';

/**
* Bundle epub to zip archive
Expand All @@ -16,17 +15,31 @@ export * from './constant';
* @returns
*/
export async function bundle(epub: Epubook): Promise<Uint8Array> {
return new Promise((res, rej) => {
return new Promise(async (res, rej) => {
const opfs = epub
.packageDocuments()
.map((opf) => [opf.filename(), fflate.strToU8(makePackageDocument(opf))] as const);

const items: Record<string, Uint8Array> = {};
for (const opf of epub.packageDocuments()) {
const base = path.dirname(opf.filename());
for (const item of opf.items()) {
const name = path.join(base, item.filename());
if (name in items) {
continue;
}
// TODO: parallel here
items[name] = await item.bundle();
}
}

const abstractContainer: fflate.AsyncZippable = {
mimetype: fflate.strToU8(MIMETYPE),
'META-INF': {
'container.xml': fflate.strToU8(makeContainer(epub))
},
...Object.fromEntries(opfs)
...Object.fromEntries(opfs),
...items
};

fflate.zip(
Expand Down Expand Up @@ -166,6 +179,8 @@ export function makePackageDocument(opf: PackageDocument): string {
return builder.build({
'?xml': { '#text': '', '@_version': '1.0', '@_encoding': 'UTF-8' },
package: {
'@_xmlns': 'http://www.idpf.org/2007/opf',
'@_xmlns:epub': 'http://www.idpf.org/2007/ops',
'@_unique-identifier': opf.uniqueIdentifier(),
'@_version': opf.version(),
metadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ export const ImagePng = 'image/png';
export const ImageSvg = 'image/svg+xml';
export const ImageWebp = 'image/webp';

export type ImageMediaType = typeof ImageGif | typeof ImageJpeg | typeof ImagePng;
export type ImageMediaType =
| typeof ImageGif
| typeof ImageJpeg
| typeof ImagePng
| typeof ImageSvg
| typeof ImageWebp;

export const TextCSS = 'text/css';

Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/epub/epub.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type PathLike, promises as fs } from 'node:fs';
import * as path from 'node:path';
import { existsSync, mkdirSync, promises as fs } from 'node:fs';

import { PackageDocument } from './opf';

Expand Down Expand Up @@ -27,8 +28,12 @@ export class Epubook {
return await bundle(this);
}

async writeFile(file: PathLike) {
async writeFile(file: string) {
const buffer = await this.bundle();
const dir = path.dirname(file);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
await fs.writeFile(file, buffer);
}
}
2 changes: 2 additions & 0 deletions packages/core/src/epub/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './epub';

export * from './opf';

export * from './item';
194 changes: 194 additions & 0 deletions packages/core/src/epub/item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import * as path from 'node:path';
import { type PathLike, promises as fs } from 'node:fs';

import { strToU8 } from 'fflate';

import {
type MediaType,
type ImageMediaType,
XHTML,
ImageJpeg,
ImagePng,
ImageWebp
} from '../constant';

export class ManifestItem {
private _href: string;

private _id: string;

private optional: {
fallback?: string;
mediaOverlay?: string;
mediaType?: MediaType;
properties?: string;
} = {};

constructor(href: string, id: string) {
this._href = href;
this._id = id;
}

public update(info: typeof this.optional) {
for (const [key, value] of Object.entries(info)) {
if (!!value) {
this.optional[key as keyof typeof this.optional] = value as any;
}
}
return this;
}

public href() {
return this._href;
}

public id() {
return this._id;
}

public fallback() {
return this.optional.fallback;
}

public mediaOverlay() {
return this.optional.mediaOverlay;
}

public mediaType() {
return this.optional.mediaType;
}

public properties() {
return this.optional.properties;
}

public ref() {
return new ManifestItemRef(this._id);
}
}

export class ManifestItemRef {
private _idref: string;

private optional: {
id?: string;

linear?: string;

properties?: string;
} = {};

constructor(idref: string) {
this._idref = idref;
}

public update(info: typeof this.optional) {
for (const [key, value] of Object.entries(info)) {
if (!!value) {
this.optional[key as keyof typeof this.optional] = value as any;
}
}
return this;
}

public idref() {
return this._idref;
}

public id() {
return this.optional.id;
}

public linear() {
return this.optional.linear;
}

public properties() {
return this.optional.properties;
}
}

export abstract class Item {
private readonly file: string;

private readonly mediaType: MediaType;

constructor(file: string, mediaType: MediaType) {
this.file = file;
this.mediaType = mediaType;
}

public filename() {
return this.file;
}

public id() {
return this.file.replace(/\/|\\/g, '_').replace(/\.[\w]+$/, '');
}

public manifest() {
return new ManifestItem(this.file, this.id()).update({ mediaType: this.mediaType });
}

public itemref() {
return this.manifest().ref();
}

abstract bundle(): Promise<Uint8Array>;
}

export class Html extends Item {
private content: string;

constructor(file: string, content: string) {
super(file, XHTML);
this.content = content;
}

static async read(src: PathLike, dst: string) {
// TODO: check src extension
const content = await fs.readFile(src, 'utf-8');
return new Html(dst, content);
}

async bundle(): Promise<Uint8Array> {
// TODO: check encode format
return strToU8(this.content);
}
}

export class Image extends Item {
private data: Uint8Array;

constructor(file: string, type: ImageMediaType, data: Uint8Array) {
super(file, type);
this.data = data;
}

static async read(src: string, dst: string) {
const content = await fs.readFile(src);
const media = this.getExtMediaType(src);
if (media) {
return new Image(dst, media, content);
} else {
return undefined;
}
}

async bundle(): Promise<Uint8Array> {
return this.data;
}

private static getExtMediaType(file: string): ImageMediaType | undefined {
const ext = path.extname(file);
if (ext === 'jpg' || ext === 'jpeg') {
return ImageJpeg;
} else if (ext === 'png') {
return ImagePng;
} else if (ext === 'webp') {
return ImageWebp;
} else {
return undefined;
}
}
}
Loading

0 comments on commit 32e23a9

Please sign in to comment.