diff --git a/.eslintignore b/.eslintignore index a261f29..0e27ca2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ dist/* +rollup* diff --git a/.eslintrc b/.eslintrc index a867b94..1f3c37a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,31 +5,41 @@ "es6": true, "node": true }, - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": 2019, - "sourceType": "module", - "ecmaFeatures": { - "jsx": true - } - }, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "standard", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], "rules": { - "indent": [ + "dot-notation": "off", + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": "error", + "no-dupe-class-members": "off", + "@typescript-eslint/no-dupe-class-members": "error", + "@typescript-eslint/no-floating-promises": "warn", + "@typescript-eslint/member-delimiter-style": "warn", + "@typescript-eslint/ban-ts-ignore": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/prefer-regexp-exec": "off", + "no-trailing-spaces": [ "error", - 4 + { + "ignoreComments": true + } ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ + "comma-dangle": [ "warn", - "double" - ], - "semi": [ - "warn", - "never" - ], - "no-console": "off" + "always-multiline" + ] + }, + "parserOptions": { + "project": [ + "./tsconfig.json" + ] } } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..17ae5d7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "eslint.options": {}, + "eslint.validate": [ + "javascript", + "typescript", + ], + "editor.tabSize": 2, + "javascript.format.insertSpaceBeforeFunctionParenthesis": true, + "typescript.format.insertSpaceBeforeFunctionParenthesis": true, + "javascript.format.insertSpaceAfterConstructor": true, + "typescript.format.insertSpaceAfterConstructor": true, + "[typescript]": { + "editor.formatOnSave": true + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, +} diff --git a/src/main.ts b/src/main.ts index 1b18e55..28c3a06 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,259 +1,262 @@ -import "./meta" +import './meta' -import { ScorePlayerData } from "./types" -import { waitForDocumentLoaded } from "./utils" -import * as recaptcha from "./recaptcha" +import { ScorePlayerData } from './types' +import { waitForDocumentLoaded } from './utils' +import * as recaptcha from './recaptcha' -import { PDFWorkerHelper } from "./worker-helper" -import FileSaver from "file-saver/dist/FileSaver.js" +import { PDFWorkerHelper } from './worker-helper' +import FileSaver from 'file-saver/dist/FileSaver.js' -const saveAs: typeof import("file-saver").saveAs = FileSaver.saveAs +const saveAs: typeof import('file-saver').saveAs = FileSaver.saveAs -const PROCESSING_TEXT = "Processing…" -const FAILED_TEXT = "❌Download Failed!" -const WEBMSCORE_URL = "https://cdn.jsdelivr.net/npm/webmscore@0.5/webmscore.js" +const PROCESSING_TEXT = 'Processing…' +const FAILED_TEXT = '❌Download Failed!' +const WEBMSCORE_URL = 'https://cdn.jsdelivr.net/npm/webmscore@0.5/webmscore.js' let pdfBlob: Blob -let msczBufferP: Promise +let msczBufferP: Promise | undefined -const generatePDF = async (imgURLs: string[], imgType: "svg" | "png", name?: string) => { - if (pdfBlob) { - return saveAs(pdfBlob, `${name}.pdf`) - } +const generatePDF = async (imgURLs: string[], imgType: 'svg' | 'png', name?: string): Promise => { + if (pdfBlob) { + return saveAs(pdfBlob, `${name}.pdf`) + } - const cachedImg = document.querySelector("img[src*=score_]") as HTMLImageElement - const { naturalWidth: width, naturalHeight: height } = cachedImg + const cachedImg: HTMLImageElement = document.querySelector('img[src*=score_]') + const { naturalWidth: width, naturalHeight: height } = cachedImg - const worker = new PDFWorkerHelper() - const pdfArrayBuffer = await worker.generatePDF(imgURLs, imgType, width, height) - worker.terminate() + const worker = new PDFWorkerHelper() + const pdfArrayBuffer = await worker.generatePDF(imgURLs, imgType, width, height) + worker.terminate() - pdfBlob = new Blob([pdfArrayBuffer]) + pdfBlob = new Blob([pdfArrayBuffer]) - saveAs(pdfBlob, `${name}.pdf`) + saveAs(pdfBlob, `${name}.pdf`) } -const getPagesNumber = (scorePlayerData: ScorePlayerData) => { - try { - return scorePlayerData.json.metadata.pages - } catch (_) { - return document.querySelectorAll("img[src*=score_]").length - } +const getPagesNumber = (scorePlayerData: ScorePlayerData): number => { + try { + return scorePlayerData.json.metadata.pages + } catch (_) { + return document.querySelectorAll('img[src*=score_]').length + } } -const getImgType = (): "svg" | "png" => { - try { - const imgE: HTMLImageElement = document.querySelector("img[src*=score_]") - const { pathname } = new URL(imgE.src) - const imgtype = pathname.match(/\.(\w+)$/)[1] - return imgtype as "svg" | "png" - } catch (_) { - return null - } +const getImgType = (): 'svg' | 'png' => { + try { + const imgE: HTMLImageElement = document.querySelector('img[src*=score_]') + const { pathname } = new URL(imgE.src) + const imgtype = pathname.match(/\.(\w+)$/)[1] + return imgtype as 'svg' | 'png' + } catch (_) { + return null + } } -const getTitle = (scorePlayerData: ScorePlayerData) => { - try { - return scorePlayerData.json.metadata.title - } catch (_) { - return "" - } +const getTitle = (scorePlayerData: ScorePlayerData): string => { + try { + return scorePlayerData.json.metadata.title + } catch (_) { + return '' + } } -const getScoreFileName = (scorePlayerData: ScorePlayerData) => { - return getTitle(scorePlayerData).replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, "_") +const getScoreFileName = (scorePlayerData: ScorePlayerData): string => { + return getTitle(scorePlayerData).replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_') } const fetchMscz = async (url: string): Promise => { - if (!msczBufferP) { - msczBufferP = (async () => { - const token = await recaptcha.execute() - const r = await fetch(url + token) - const data = await r.arrayBuffer() - return data - })() - } + if (!msczBufferP) { + msczBufferP = (async (): Promise => { + const token = await recaptcha.execute() + const r = await fetch(url + token) + const data = await r.arrayBuffer() + return data + })() + } - return msczBufferP + return msczBufferP } -const main = () => { +const main = (): void => { + // @ts-ignore + if (!window.UGAPP || !window.UGAPP.store || !window.UGAPP.store.jmuse_settings) { return } - // @ts-ignore - if (!window.UGAPP || !window.UGAPP.store || !window.UGAPP.store.jmuse_settings) { return } + // init recaptcha + // eslint-disable-next-line @typescript-eslint/no-floating-promises + recaptcha.init() - // init recaptcha - recaptcha.init() + // @ts-ignore + const scorePlayer: ScorePlayerData = window.UGAPP.store.jmuse_settings.score_player - // @ts-ignore - const scorePlayer: ScorePlayerData = window.UGAPP.store.jmuse_settings.score_player + const { id } = scorePlayer.json + const baseURL = scorePlayer.urls.image_path - const { id } = scorePlayer.json - const baseURL = scorePlayer.urls.image_path + const filename = getScoreFileName(scorePlayer) - const filename = getScoreFileName(scorePlayer) + // https://github.com/Xmader/cloudflare-worker-musescore-mscz + const msczURL = `https://musescore.now.sh/api/mscz?id=${id}&token=` - // https://github.com/Xmader/cloudflare-worker-musescore-mscz - const msczURL = `https://musescore.now.sh/api/mscz?id=${id}&token=` + const mxlURL = baseURL + 'score.mxl' + const { midi: midiURL, mp3: mp3URL } = scorePlayer.urls - const mxlURL = baseURL + "score.mxl" - const { midi: midiURL, mp3: mp3URL } = scorePlayer.urls + const btnsDiv = document.querySelector('.score-right .buttons-wrapper') || document.querySelectorAll('aside section > div')[4] + const downloadBtn = btnsDiv.querySelector('button, .button') as HTMLElement + downloadBtn.onclick = null - const btnsDiv = document.querySelector(".score-right .buttons-wrapper") || document.querySelectorAll("aside section > div")[4] - const downloadBtn = btnsDiv.querySelector("button, .button") as HTMLElement - downloadBtn.onclick = null + // fix the icon of the download btn + // if the `downloadBtn` seleted was a `Print` btn, replace the `print` icon with the `download` icon + const svgPath: SVGPathElement = downloadBtn.querySelector('svg > path') + if (svgPath) { + svgPath.setAttribute('d', 'M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z') + } - // fix the icon of the download btn - // if the `downloadBtn` seleted was a `Print` btn, replace the `print` icon with the `download` icon - const svgPath: SVGPathElement = downloadBtn.querySelector("svg > path") - if (svgPath) { - svgPath.setAttribute("d", "M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z") + const imgType = getImgType() || 'svg' + + const sheetImgURLs = Array.from({ length: getPagesNumber(scorePlayer) }).fill(null).map((_, i) => { + return baseURL + `score_${i}.${imgType}` + }) + + const downloadURLs = { + MSCZ: null, + PDF: null, + MusicXML: mxlURL, + MIDI: midiURL, + MP3: mp3URL, + Parts: null, + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const createBtn = (name: string) => { + const btn: HTMLButtonElement = downloadBtn.cloneNode(true) as any + + if (btn.nodeName.toLowerCase() === 'button') { + btn.setAttribute('style', 'width: 205px !important') + } else { + btn.dataset.target = '' } - const imgType = getImgType() || "svg" - - const sheetImgURLs = Array.from({ length: getPagesNumber(scorePlayer) }).fill(null).map((_, i) => { - return baseURL + `score_${i}.${imgType}` + const textNode = [...btn.childNodes].find((x) => { + return x.textContent.includes('Download') || x.textContent.includes('Print') }) + textNode.textContent = `Download ${name}` - const downloadURLs = { - "MSCZ": null, - "PDF": null, - "MusicXML": mxlURL, - "MIDI": midiURL, - "MP3": mp3URL, - "Parts": null, + return { + btn, + textNode, } + } - const createBtn = (name: string) => { - const btn = downloadBtn.cloneNode(true) as HTMLElement + const newDownloadBtns = Object.keys(downloadURLs).map((name) => { + const url = downloadURLs[name] + const { btn, textNode } = createBtn(name) - if (btn.nodeName.toLowerCase() == "button") { - btn.setAttribute("style", "width: 205px !important") - } else { - btn.dataset.target = "" + if (name === 'PDF') { + btn.onclick = async (): Promise => { + const filename = getScoreFileName(scorePlayer) + + textNode.textContent = PROCESSING_TEXT + + try { + await generatePDF(sheetImgURLs, imgType, filename) + textNode.textContent = 'Download PDF' + } catch (err) { + textNode.textContent = FAILED_TEXT + console.error(err) } + } + } else if (name === 'MSCZ') { + btn.onclick = async (): Promise => { + textNode.textContent = PROCESSING_TEXT - const textNode = [...btn.childNodes].find((x) => { - return x.textContent.includes("Download") || x.textContent.includes("Print") + try { + const data = new Blob([await fetchMscz(msczURL)]) + textNode.textContent = 'Download MSCZ' + saveAs(data, `${filename}.mscz`) + } catch (err) { + textNode.textContent = FAILED_TEXT + console.error(err) + } + } + } else if (name === 'Parts') { // download individual parts + btn.title = 'Download individual parts (BETA)' + const cb = btn.onclick = async (): Promise => { + btn.onclick = null + textNode.textContent = PROCESSING_TEXT + + const w = window.open('') + const txt = document.createTextNode(PROCESSING_TEXT) + w.document.body.append(txt) + + // set page hooks + // eslint-disable-next-line prefer-const + let score: any + const destroy = (): void => { + score.destroy() + w.close() + } + window.addEventListener('unload', destroy) + w.addEventListener('beforeunload', () => { + score.destroy() + window.removeEventListener('unload', destroy) + textNode.textContent = 'Download Parts' + btn.onclick = cb }) - textNode.textContent = `Download ${name}` - return { - btn, - textNode, + // load webmscore (https://github.com/LibreScore/webmscore) + const script = w.document.createElement('script') + script.src = WEBMSCORE_URL + w.document.body.append(script) + await new Promise(resolve => { script.onload = resolve }) + + // parse mscz data + const data = new Uint8Array( + new Uint8Array(await fetchMscz(msczURL)) // copy its ArrayBuffer + ) + score = await w['WebMscore'].load('mscz', data) + await score.generateExcerpts() + const metadata = await score.metadata() + console.log('score metadata loaded by webmscore', metadata) + + // render the part selection page + txt.remove() + const fieldset = w.document.createElement('fieldset') + for (const excerpt of metadata.excerpts) { + const e = w.document.createElement('input') + e.name = 'score-part' + e.type = 'radio' + e.value = excerpt.id + const label = w.document.createElement('label') + label.innerText = excerpt.title + const br = w.document.createElement('br') + fieldset.append(e, label, br) } + const submitBtn = w.document.createElement('input') + submitBtn.type = 'submit' + submitBtn.value = 'Download PDF' + fieldset.append(submitBtn) + w.document.body.append(fieldset) + + submitBtn.onclick = async (): Promise => { + const checked: HTMLInputElement = fieldset.querySelector('input:checked') + const id = checked.value + + await score.setExcerptId(id) + + const data = new Blob([await score.savePdf()]) + saveAs(data, `${filename}-part-${id}.pdf`) + } + } + } else { + btn.onclick = (): void => { + window.open(url) + } } - const newDownloadBtns = Object.keys(downloadURLs).map((name) => { - const url = downloadURLs[name] - const { btn, textNode } = createBtn(name) - - if (name == "PDF") { - btn.onclick = async () => { - const filename = getScoreFileName(scorePlayer) - - textNode.textContent = PROCESSING_TEXT - - try { - await generatePDF(sheetImgURLs, imgType, filename) - textNode.textContent = "Download PDF" - } catch (err) { - textNode.textContent = FAILED_TEXT - console.error(err) - } - } - } else if (name == "MSCZ") { - btn.onclick = async () => { - textNode.textContent = PROCESSING_TEXT - - try { - const data = new Blob([await fetchMscz(msczURL)]) - textNode.textContent = "Download MSCZ" - saveAs(data, `${filename}.mscz`) - } catch (err) { - textNode.textContent = FAILED_TEXT - console.error(err) - } - } - } else if (name == "Parts") { // download individual parts - btn.title = "Download individual parts (BETA)" - const cb = btn.onclick = async () => { - btn.onclick = null - textNode.textContent = PROCESSING_TEXT - - const w = window.open("") - const txt = document.createTextNode(PROCESSING_TEXT) - w.document.body.append(txt) - - // set page hooks - const destroy = () => { - score.destroy() - w.close() - } - window.addEventListener("unload", destroy) - w.addEventListener("beforeunload", () => { - score.destroy() - window.removeEventListener("unload", destroy) - textNode.textContent = "Download Parts" - btn.onclick = cb - }) - - // load webmscore (https://github.com/LibreScore/webmscore) - const script = w.document.createElement("script") - script.src = WEBMSCORE_URL - w.document.body.append(script) - await new Promise(resolve => { script.onload = resolve }) - - // parse mscz data - const data = new Uint8Array( - new Uint8Array(await fetchMscz(msczURL)) // copy its ArrayBuffer - ) - const score = await w["WebMscore"].load("mscz", data) - await score.generateExcerpts() - const metadata = await score.metadata() - console.log("score metadata loaded by webmscore", metadata) - - // render the part selection page - txt.remove() - const fieldset = w.document.createElement("fieldset") - for (const excerpt of metadata.excerpts) { - const e = w.document.createElement("input") - e.name = "score-part" - e.type = "radio" - e.value = excerpt.id - const label = w.document.createElement("label") - label.innerText = excerpt.title - const br = w.document.createElement("br") - fieldset.append(e, label, br) - } - const submitBtn = w.document.createElement("input") - submitBtn.type = "submit" - submitBtn.value = "Download PDF" - fieldset.append(submitBtn) - w.document.body.append(fieldset) - - submitBtn.onclick = async () => { - const checked: HTMLInputElement = w.document.querySelector("input:checked") - const id = checked.value - - await score.setExcerptId(id) - - const data = new Blob([await score.savePdf()]) - saveAs(data, `${filename}-part-${id}.pdf`) - } - } - } else { - btn.onclick = () => { - window.open(url) - } - } - - return btn - }) - - downloadBtn.replaceWith(...newDownloadBtns) + return btn + }) + downloadBtn.replaceWith(...newDownloadBtns) } +// eslint-disable-next-line @typescript-eslint/no-floating-promises waitForDocumentLoaded().then(main) diff --git a/src/recaptcha.ts b/src/recaptcha.ts index 76b7cc7..fdcb063 100644 --- a/src/recaptcha.ts +++ b/src/recaptcha.ts @@ -2,12 +2,12 @@ /** * the site key for Google reCAPTCHA v3 */ -const SITE_KEY = "6Ldxtt8UAAAAALvcRqWTlVOVIB7MmEWwN-zw_9fM" +const SITE_KEY = '6Ldxtt8UAAAAALvcRqWTlVOVIB7MmEWwN-zw_9fM' type token = string; interface GRecaptcha { - ready(cb: Function): void; - execute(siteKey: string, opts: { action: string }): Promise; + ready (cb: Function): void; + execute (siteKey: string, opts: { action: string }): Promise; } let gr: GRecaptcha | Promise @@ -16,33 +16,33 @@ let gr: GRecaptcha | Promise * load reCAPTCHA */ const load = (): Promise => { - // load script - const script = document.createElement("script") - script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${SITE_KEY}` - script.async = true - document.body.appendChild(script) + // load script + const script = document.createElement('script') + script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${SITE_KEY}` + script.async = true + document.body.appendChild(script) - // add css - const style = document.createElement("style") - style.innerHTML = ".grecaptcha-badge { display: none !important; }" - document.head.appendChild(style) + // add css + const style = document.createElement('style') + style.innerHTML = '.grecaptcha-badge { display: none !important; }' + document.head.appendChild(style) - return new Promise((resolve) => { - script.onload = () => { - const grecaptcha: GRecaptcha = window["grecaptcha"] - grecaptcha.ready(() => resolve(grecaptcha)) - } - }) + return new Promise((resolve) => { + script.onload = (): void => { + const grecaptcha: GRecaptcha = window['grecaptcha'] + grecaptcha.ready(() => resolve(grecaptcha)) + } + }) } -export const init = () => { - if (!gr) { - gr = load() - } - return gr +export const init = (): GRecaptcha | Promise => { + if (!gr) { + gr = load() + } + return gr } export const execute = async (): Promise => { - const captcha = await init() - return captcha.execute(SITE_KEY, { action: "downloadmscz" }) + const captcha = await init() + return captcha.execute(SITE_KEY, { action: 'downloadmscz' }) } diff --git a/src/types.ts b/src/types.ts index 15b63cd..057bdcc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,84 +1,84 @@ interface SourceData { - type: string; // "audio" - title: string; // "Musescore audio" - nid: number; + type: string; // "audio" + title: string; // "Musescore audio" + nid: number; } type CommentData = any; interface PartData { - part: { - name: string; - program: number; - } + part: { + name: string; + program: number; + }; } interface Metadata { - title: string; - subtitle?: string; - composer?: string; - poet?: string; - pages: number; - measures: number; - lyrics: number; - chordnames: number; - keysig: number; - duration: number; - dimensions: number; - parts: PartData[]; + title: string; + subtitle?: string; + composer?: string; + poet?: string; + pages: number; + measures: number; + lyrics: number; + chordnames: number; + keysig: number; + duration: number; + dimensions: number; + parts: PartData[]; } interface ScoreJson { - id: number; - vid: number; - dates: { - revised: number; - }; - secret: string; - permalink: string; - custom_url: string; - format: string; // "0" - has_custom_audio: 0 | 1; - metadata: Metadata; + id: number; + vid: number; + dates: { + revised: number; + }; + secret: string; + permalink: string; + custom_url: string; + format: string; // "0" + has_custom_audio: 0 | 1; + metadata: Metadata; } interface UrlsData { - midi: string; - mp3: string; - space: string; - image_path: string; - media?: string[]; + midi: string; + mp3: string; + space: string; + image_path: string; + media?: string[]; } interface AccessControlData { - enabled: boolean; - hasAccess: boolean; + enabled: boolean; + hasAccess: boolean; } interface PianoKeyboardData extends AccessControlData { - midiUrl: string; + midiUrl: string; } interface PianoRollData extends AccessControlData { - resourcesUrl: string; - feedbackUrl: string; - forceShow: boolean; + resourcesUrl: string; + feedbackUrl: string; + forceShow: boolean; } export interface ScorePlayerData { - embed: boolean; - sources: SourceData[]; - default_source?: SourceData; - mixer?: string; - secondaryMixer?: string; - bucket?: string; // "https://musescore.com/static/musescore/scoredata" - json: ScoreJson; - render_vector: boolean; - comments: CommentData[]; - score_id: number; - urls: UrlsData; - sendEvents?: boolean; - pianoKeyboard: PianoKeyboardData; - pianoRoll: PianoRollData; + embed: boolean; + sources: SourceData[]; + default_source?: SourceData; + mixer?: string; + secondaryMixer?: string; + bucket?: string; // "https://musescore.com/static/musescore/scoredata" + json: ScoreJson; + render_vector: boolean; + comments: CommentData[]; + score_id: number; + urls: UrlsData; + sendEvents?: boolean; + pianoKeyboard: PianoKeyboardData; + pianoRoll: PianoRollData; } diff --git a/src/utils.ts b/src/utils.ts index 8282a62..942e28f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,24 +1,24 @@ -export const getIndexPath = (id: number) => { - const idStr = String(id) - // 获取最后三位,倒序排列 - // x, y, z are the reversed last digits of the score id. Example: id 123456789, x = 9, y = 8, z = 7 - // https://developers.musescore.com/#/file-urls - // "5449062" -> ["2", "6", "0"] - const indexN = idStr.split("").reverse().slice(0, 3) - return indexN.join("/") +export const getIndexPath = (id: number): string => { + const idStr = String(id) + // 获取最后三位,倒序排列 + // x, y, z are the reversed last digits of the score id. Example: id 123456789, x = 9, y = 8, z = 7 + // https://developers.musescore.com/#/file-urls + // "5449062" -> ["2", "6", "0"] + const indexN = idStr.split('').reverse().slice(0, 3) + return indexN.join('/') } export const waitForDocumentLoaded = (): Promise => { - if (document.readyState !== "complete") { - return new Promise(resolve => { - document.addEventListener("readystatechange", () => { - if (document.readyState == "complete") { - resolve() - } - }, { once: true }) - }) - } else { - return Promise.resolve() - } + if (document.readyState !== 'complete') { + return new Promise(resolve => { + document.addEventListener('readystatechange', () => { + if (document.readyState === 'complete') { + resolve() + } + }, { once: true }) + }) + } else { + return Promise.resolve() + } } diff --git a/src/worker-helper.ts b/src/worker-helper.ts index dde117a..bd9e620 100644 --- a/src/worker-helper.ts +++ b/src/worker-helper.ts @@ -1,30 +1,30 @@ -import { PDFWorkerMessage } from "./worker" -import { PDFWorker } from "../dist/cache/worker" +import { PDFWorkerMessage } from './worker' +import { PDFWorker } from '../dist/cache/worker' -const scriptUrlFromFunction = (fn: Function) => { - const blob = new Blob(["(" + fn.toString() + ")()"], { type: "application/javascript" }) - return URL.createObjectURL(blob) +const scriptUrlFromFunction = (fn: Function): string => { + const blob = new Blob(['(' + fn.toString() + ')()'], { type: 'application/javascript' }) + return URL.createObjectURL(blob) } export class PDFWorkerHelper extends Worker { - constructor() { - const url = scriptUrlFromFunction(PDFWorker) - super(url) - } + constructor () { + const url = scriptUrlFromFunction(PDFWorker) + super(url) + } - generatePDF(imgURLs: string[], imgType: "svg" | "png", width: number, height: number): Promise { - const msg: PDFWorkerMessage = [ - imgURLs, - imgType, - width, - height, - ] - this.postMessage(msg) - return new Promise((resolve) => { - this.addEventListener("message", (e) => { - resolve(e.data) - }) - }) - } + generatePDF (imgURLs: string[], imgType: 'svg' | 'png', width: number, height: number): Promise { + const msg: PDFWorkerMessage = [ + imgURLs, + imgType, + width, + height, + ] + this.postMessage(msg) + return new Promise((resolve) => { + this.addEventListener('message', (e) => { + resolve(e.data) + }) + }) + } } diff --git a/src/worker.ts b/src/worker.ts index b93fe2b..449164e 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,88 +1,87 @@ /// -import PDFDocument from "pdfkit/lib/document" -import SVGtoPDF from "svg-to-pdfkit" +import PDFDocument from 'pdfkit/lib/document' +import SVGtoPDF from 'svg-to-pdfkit' -type ImgType = "svg" | "png" - -const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, height: number): Promise => { - - // @ts-ignore - const pdf = new (PDFDocument as typeof import("pdfkit"))({ - // compress: true, - size: [width, height], - autoFirstPage: false, - margin: 0, - layout: "portrait", - }) - - if (imgType == "png") { - const imgDataUrlList: string[] = await Promise.all(imgURLs.map(fetchDataURL)) - - imgDataUrlList.forEach((data) => { - pdf.addPage() - pdf.image(data, { - width, - height, - }) - }) - } else { // imgType == "svg" - const svgList = await Promise.all(imgURLs.map(fetchText)) - - svgList.forEach((svg) => { - pdf.addPage() - SVGtoPDF(pdf, svg, 0, 0, { - preserveAspectRatio: "none", - }) - }) - } - - // @ts-ignore - const buf: Uint8Array = await pdf.getBuffer() - - return buf.buffer -} +type ImgType = 'svg' | 'png' const getDataURL = (blob: Blob): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = () => { - const result = reader.result - resolve(result as string) - } - reader.onerror = reject - reader.readAsDataURL(blob) - }) + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (): void => { + const result = reader.result + resolve(result as string) + } + reader.onerror = reject + reader.readAsDataURL(blob) + }) } const fetchDataURL = async (imgUrl: string): Promise => { - const r = await fetch(imgUrl) - const blob = await r.blob() - return getDataURL(blob) + const r = await fetch(imgUrl) + const blob = await r.blob() + return getDataURL(blob) } const fetchText = async (imgUrl: string): Promise => { - const r = await fetch(imgUrl) - return r.text() + const r = await fetch(imgUrl) + return r.text() +} + +const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, height: number): Promise => { + // @ts-ignore + const pdf = new (PDFDocument as typeof import('pdfkit'))({ + // compress: true, + size: [width, height], + autoFirstPage: false, + margin: 0, + layout: 'portrait', + }) + + if (imgType === 'png') { + const imgDataUrlList: string[] = await Promise.all(imgURLs.map(fetchDataURL)) + + imgDataUrlList.forEach((data) => { + pdf.addPage() + pdf.image(data, { + width, + height, + }) + }) + } else { // imgType == "svg" + const svgList = await Promise.all(imgURLs.map(fetchText)) + + svgList.forEach((svg) => { + pdf.addPage() + SVGtoPDF(pdf, svg, 0, 0, { + preserveAspectRatio: 'none', + }) + }) + } + + // @ts-ignore + const buf: Uint8Array = await pdf.getBuffer() + + return buf.buffer } export type PDFWorkerMessage = [string[], ImgType, number, number]; -onmessage = async (e) => { - const [ - imgURLs, - imgType, - width, - height, - ] = e.data as PDFWorkerMessage +onmessage = async (e): Promise => { + const [ + imgURLs, + imgType, + width, + height, + ] = e.data as PDFWorkerMessage - const pdfBuf = await generatePDF( - imgURLs, - imgType, - width, - height, - ) + const pdfBuf = await generatePDF( + imgURLs, + imgType, + width, + height, + ) - postMessage(pdfBuf, [pdfBuf]) + postMessage(pdfBuf, [pdfBuf]) } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2f02d4e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": [ + "dom", + "es2019", + ], + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + "strictNullChecks": true, + "sourceMap": false, + "newLine": "lf", + } +} \ No newline at end of file