-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Add logic for inserting iscn page into epub
- Loading branch information
1 parent
ac2ba71
commit 362f984
Showing
7 changed files
with
259 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
export const ISCN_CSS = `body { | ||
display: flex; | ||
flex-direction: column; | ||
height: 100vh; | ||
font-family: sans-serif; | ||
text-align: center; | ||
} | ||
#iscn-page-body { | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: center; | ||
flex-grow: 1; | ||
padding: 0.5em 2vw 4em; | ||
} | ||
#iscn-page-footer { | ||
border-top: 1px solid #ccc; | ||
padding: 0.5em; | ||
} | ||
#iscn-qr-code { | ||
margin-top: 0.5em; | ||
} | ||
#iscn-prefix { | ||
font-size: 0.75em; | ||
font-family: monospace; | ||
word-break: break-all; | ||
}` | ||
|
||
export const ISCN_XHTML = `<?xml version="1.0" encoding="utf-8"?> | ||
<!DOCTYPE html> | ||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> | ||
<head> | ||
<title>ISCN on LikeCoin Chain</title> | ||
<link href="iscn.css" type="text/css" rel="stylesheet" /> | ||
</head> | ||
<body> | ||
<div id="iscn-page-body"> | ||
<div id="iscn-page-book-info" /> | ||
<div id="iscn-qr-code"> | ||
<img src="iscn-qr-code.png" width="180px" height="180px" /> | ||
<br /> | ||
<a id="iscn-prefix" /> | ||
</div> | ||
</div> | ||
<div id="iscn-page-footer"> | ||
<p id="depub-disclaimer" /> | ||
</div> | ||
</body> | ||
</html> | ||
` | ||
|
||
export const ISCN_LOGO_SVG = `<svg width="1024" height="1024" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> | ||
<path d="M2.76,14.79H3.92V9.34H2.76Zm7.42-2.72a2.76,2.76,0,0,1,2.7-2.82H13a2.52,2.52,0,0,1,2.32,1.33l-1,.49a1.48,1.48,0,0,0-1.32-.8A1.69,1.69,0,0,0,11.37,12v0A1.7,1.7,0,0,0,13,13.81H13A1.49,1.49,0,0,0,14.36,13l1,.48A2.53,2.53,0,0,1,13,14.84a2.75,2.75,0,0,1-2.86-2.62ZM5.53,13.14a2.4,2.4,0,0,0,1.73.73c.64,0,1-.3,1-.61s-.47-.55-1.1-.69c-.89-.21-2-.45-2-1.67,0-.91.79-1.64,2.07-1.64A3,3,0,0,1,9.27,10l-.65.85a2.31,2.31,0,0,0-1.56-.6c-.52,0-.8.23-.8.56s.46.48,1.09.63c.9.2,2,.47,2,1.68,0,1-.71,1.75-2.18,1.75A3.1,3.1,0,0,1,4.9,14Zm12-2V14.8H16.32V9.34h1.19L20,12.87V9.34H21.2v5.45H20.08ZM8.68,8.22c1.3-.81,3-1.71,3.79-2.2A1.13,1.13,0,0,1,13,5.93a1.17,1.17,0,0,1,.45.09c.84.45,2.47,1.35,3.78,2.16h1.42a1.22,1.22,0,0,0-.32-.28c-1.4-.9-3.51-2.08-4.5-2.6a1.8,1.8,0,0,0-.87-.23,1.73,1.73,0,0,0-.86.23c-1,.52-3.1,1.7-4.5,2.6l-.39.28Zm8.38,7.69c-1.28.78-2.82,1.64-3.63,2.07a1.1,1.1,0,0,1-.47.13,1.19,1.19,0,0,1-.49-.13c-.81-.43-2.35-1.29-3.64-2.07H7.32l.27.19c1.4.9,3.51,2.08,4.5,2.6a1.73,1.73,0,0,0,1.73,0c1-.52,3.1-1.7,4.5-2.6a1.24,1.24,0,0,0,.22-.18H17.06Z" fill="white"/> | ||
</svg> | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import QRCode from 'qrcode' | ||
import JSZip from 'jszip' | ||
import { Book } from 'epubjs' | ||
|
||
import { SITE_URL } from '~/constant' | ||
import { ISCN_CSS, ISCN_LOGO_SVG, ISCN_XHTML } from './asset' | ||
import { extractIscnIdPrefix } from '../ui' | ||
|
||
const LIKE_GREEN = '#28646e' | ||
const QR_CODE_SIZE = 256 | ||
const LOGO_SIZE = 64 | ||
|
||
const ISCN_CSS_NAME = 'iscn.css' | ||
const ISCN_XHTML_NAME = 'iscn.xhtml' | ||
const ISCN_QR_CODE_PNG_NAME = 'iscn-qr-code.png' | ||
|
||
function loadImage(url: string): Promise<HTMLImageElement> { | ||
return new Promise((resolve, reject) => { | ||
const i = new Image(); | ||
i.onload = (() => resolve(i)); | ||
i.onerror = (e => reject(e)); | ||
i.src = url; | ||
}); | ||
} | ||
|
||
function getISCNURL(iscnPrefix: string) { | ||
return `${SITE_URL}/view/${encodeURIComponent(iscnPrefix)}` | ||
} | ||
|
||
async function createQRCodeCanvas(iscnPrefix: string) { | ||
const iscnURL = getISCNURL(iscnPrefix) | ||
const initQRCode = await QRCode.toDataURL(iscnURL, { | ||
color: { | ||
light: LIKE_GREEN, | ||
dark: '#fff', | ||
}, | ||
errorCorrectionLevel: 'H', | ||
margin: 2, | ||
}) | ||
const canvas = document.createElement('canvas') | ||
canvas.width = QR_CODE_SIZE | ||
canvas.height = QR_CODE_SIZE | ||
const ctx = canvas.getContext('2d') | ||
if (!ctx) throw new Error('Cannot get 2d context') | ||
|
||
const img = await loadImage(initQRCode) | ||
ctx.drawImage(img, 0, 0, QR_CODE_SIZE, QR_CODE_SIZE) | ||
|
||
// draw blank white circle | ||
const circleCenter = canvas.width / 2 | ||
ctx.fillStyle = LIKE_GREEN | ||
ctx.beginPath() | ||
ctx.arc(circleCenter, circleCenter, LOGO_SIZE / 2 + 2, 0, 2 * Math.PI, false) | ||
ctx.fill() | ||
|
||
// draw logo | ||
const logoBlob = new Blob([ISCN_LOGO_SVG], {type: 'image/svg+xml'}); | ||
const logoUrl = URL.createObjectURL(logoBlob); | ||
const logoImage = await loadImage(logoUrl) | ||
URL.revokeObjectURL(logoUrl) | ||
const logoPosition = (canvas.width - LOGO_SIZE) / 2 | ||
ctx.drawImage(logoImage, logoPosition, logoPosition, LOGO_SIZE, LOGO_SIZE) | ||
|
||
return canvas | ||
} | ||
|
||
function saveCanvas(canvas: HTMLCanvasElement): Promise<Blob> { | ||
return new Promise((resolve, reject) => { | ||
canvas.toBlob((blob: Blob | null) => blob ? resolve(blob) : reject(new Error('Cannot save canvas to blob'))) | ||
}) | ||
} | ||
|
||
function updateContentOPF(doc: Document, iscnPageHref: string, iscnQRCodeHref: string) { | ||
if (doc.querySelector('#iscn-page')) return | ||
const manifest = doc.querySelector('manifest') | ||
manifest?.insertAdjacentHTML('beforeend', ` <item id="iscn-css" href="iscn.css" media-type="text/css" />\n `) | ||
manifest?.insertAdjacentHTML('beforeend', ` <item id="iscn-qr-code-image" href="${iscnQRCodeHref}" media-type="image/png" />\n `) | ||
manifest?.insertAdjacentHTML('beforeend', ` <item id="iscn-page" href="${iscnPageHref}" media-type="application/xhtml+xml" />\n `) | ||
|
||
const spine = doc.querySelector('spine') | ||
spine?.insertAdjacentHTML('beforeend', ` <itemref idref="iscn-page" />\n `) | ||
} | ||
|
||
function readInfoMap(doc: Document) { | ||
const titles = doc.querySelector("metadata title") | ||
const title = titles?.textContent | ||
const authors = doc.querySelector("metadata creator") | ||
const author = authors?.textContent | ||
const releaseDate = new Date().toLocaleDateString('en-US', { | ||
year: 'numeric', | ||
month: 'long', | ||
day: 'numeric', | ||
}) | ||
const map = new Map() | ||
map.set('Title', title) | ||
map.set('Author', author) | ||
map.set('Release Date', releaseDate) | ||
return map | ||
} | ||
|
||
function addBookInfo(doc: Document, infoItemMap: Map<string, string>) { | ||
const div = doc.querySelector('body #iscn-page-book-info') | ||
const itemsString = [...infoItemMap] | ||
.filter(([ | ||
key, | ||
value, | ||
]) => key && value) | ||
.map(([ | ||
key, | ||
value, | ||
]) => ( | ||
`<p>${key}: ${value}</p>\n` | ||
)) | ||
.join('') | ||
div?.insertAdjacentHTML('afterbegin', itemsString) | ||
} | ||
|
||
function setISCNLink(doc: Document, iscnPrefix: string) { | ||
const a = doc.querySelector('body a#iscn-prefix') | ||
const iscnURL = getISCNURL(iscnPrefix) | ||
a?.setAttribute('href', iscnURL) | ||
if (a) a.textContent = iscnPrefix | ||
} | ||
|
||
function addFooterDisclaimer(doc: Document) { | ||
const footer = doc.querySelector('body #depub-disclaimer') | ||
if (footer) { | ||
footer.textContent = 'This book is published on decentralized networks' | ||
} | ||
} | ||
|
||
export async function injectISCNQRCodePage(buffer: ArrayBuffer, book: Book, iscnId: string) { | ||
const zipObject = new JSZip() | ||
await zipObject.loadAsync(buffer) | ||
const iscnPrefix = extractIscnIdPrefix(iscnId) | ||
// create QR code | ||
const canvas = await createQRCodeCanvas(iscnPrefix) | ||
const oebpsPath: string = (book.container as any).directory | ||
const blob = await saveCanvas(canvas) | ||
if (!blob) throw new Error('Cannot save canvas to blob') | ||
await zipObject.file(`${oebpsPath}/${ISCN_QR_CODE_PNG_NAME}`, blob) | ||
|
||
// read and update content.opf | ||
const path = (book.container as any).packagePath | ||
const opfString = await zipObject.file((book.container as any).packagePath)?.async('string') || '' | ||
const doc = new DOMParser().parseFromString(opfString, 'text/xml') | ||
updateContentOPF(doc, ISCN_XHTML_NAME, ISCN_QR_CODE_PNG_NAME) | ||
const infoMap = readInfoMap(doc) | ||
const updatedOpfString = new XMLSerializer().serializeToString(doc).toString() | ||
await zipObject.file(path, updatedOpfString) | ||
|
||
// add ISCN css | ||
await zipObject.file(`${oebpsPath}/${ISCN_CSS_NAME}`, ISCN_CSS) | ||
|
||
// add ISCN XHTML and update info | ||
const iscnXHTMLPath = `${oebpsPath}/${ISCN_XHTML_NAME}` | ||
const iscnXHTMLDoc = new DOMParser().parseFromString(ISCN_XHTML, 'text/xml') | ||
addBookInfo(iscnXHTMLDoc, infoMap) | ||
setISCNLink(iscnXHTMLDoc, iscnPrefix) | ||
addFooterDisclaimer(iscnXHTMLDoc) | ||
const updatedISCNXHTMLString = new XMLSerializer().serializeToString(iscnXHTMLDoc).toString() | ||
|
||
await zipObject.file(iscnXHTMLPath, updatedISCNXHTMLString) | ||
const epubBlob = await zipObject.generateAsync({ | ||
mimeType: 'application/epub+zip', | ||
type: 'blob', | ||
compression: 'DEFLATE', | ||
compressionOptions: { | ||
level: 9, | ||
}, | ||
}) | ||
return epubBlob | ||
} | ||
|
||
export default injectISCNQRCodePage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters