style: use JavaScript Standard Style

This commit is contained in:
Xmader 2020-05-17 18:57:28 -04:00
parent 0fc5b0d990
commit 435c4427f3
10 changed files with 467 additions and 420 deletions

View File

@ -1 +1,2 @@
dist/* dist/*
rollup*

View File

@ -5,31 +5,41 @@
"es6": true, "es6": true,
"node": true "node": true
}, },
"extends": "eslint:recommended", "parser": "@typescript-eslint/parser",
"parserOptions": { "plugins": [
"ecmaVersion": 2019, "@typescript-eslint"
"sourceType": "module", ],
"ecmaFeatures": { "extends": [
"jsx": true "standard",
} "plugin:@typescript-eslint/recommended",
}, "plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": { "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", "error",
4 {
"ignoreComments": true
}
], ],
"linebreak-style": [ "comma-dangle": [
"error",
"unix"
],
"quotes": [
"warn", "warn",
"double" "always-multiline"
], ]
"semi": [ },
"warn", "parserOptions": {
"never" "project": [
], "./tsconfig.json"
"no-console": "off" ]
} }
} }

18
.vscode/settings.json vendored Normal file
View File

@ -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
},
}

View File

@ -1,259 +1,262 @@
import "./meta" import './meta'
import { ScorePlayerData } from "./types" import { ScorePlayerData } from './types'
import { waitForDocumentLoaded } from "./utils" import { waitForDocumentLoaded } from './utils'
import * as recaptcha from "./recaptcha" import * as recaptcha from './recaptcha'
import { PDFWorkerHelper } from "./worker-helper" import { PDFWorkerHelper } from './worker-helper'
import FileSaver from "file-saver/dist/FileSaver.js" 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 PROCESSING_TEXT = 'Processing…'
const FAILED_TEXT = "❌Download Failed!" const FAILED_TEXT = '❌Download Failed!'
const WEBMSCORE_URL = "https://cdn.jsdelivr.net/npm/webmscore@0.5/webmscore.js" const WEBMSCORE_URL = 'https://cdn.jsdelivr.net/npm/webmscore@0.5/webmscore.js'
let pdfBlob: Blob let pdfBlob: Blob
let msczBufferP: Promise<ArrayBuffer> let msczBufferP: Promise<ArrayBuffer> | undefined
const generatePDF = async (imgURLs: string[], imgType: "svg" | "png", name?: string) => { const generatePDF = async (imgURLs: string[], imgType: 'svg' | 'png', name?: string): Promise<void> => {
if (pdfBlob) { if (pdfBlob) {
return saveAs(pdfBlob, `${name}.pdf`) return saveAs(pdfBlob, `${name}.pdf`)
} }
const cachedImg = document.querySelector("img[src*=score_]") as HTMLImageElement const cachedImg: HTMLImageElement = document.querySelector('img[src*=score_]')
const { naturalWidth: width, naturalHeight: height } = cachedImg const { naturalWidth: width, naturalHeight: height } = cachedImg
const worker = new PDFWorkerHelper() const worker = new PDFWorkerHelper()
const pdfArrayBuffer = await worker.generatePDF(imgURLs, imgType, width, height) const pdfArrayBuffer = await worker.generatePDF(imgURLs, imgType, width, height)
worker.terminate() worker.terminate()
pdfBlob = new Blob([pdfArrayBuffer]) pdfBlob = new Blob([pdfArrayBuffer])
saveAs(pdfBlob, `${name}.pdf`) saveAs(pdfBlob, `${name}.pdf`)
} }
const getPagesNumber = (scorePlayerData: ScorePlayerData) => { const getPagesNumber = (scorePlayerData: ScorePlayerData): number => {
try { try {
return scorePlayerData.json.metadata.pages return scorePlayerData.json.metadata.pages
} catch (_) { } catch (_) {
return document.querySelectorAll("img[src*=score_]").length return document.querySelectorAll('img[src*=score_]').length
} }
} }
const getImgType = (): "svg" | "png" => { const getImgType = (): 'svg' | 'png' => {
try { try {
const imgE: HTMLImageElement = document.querySelector("img[src*=score_]") const imgE: HTMLImageElement = document.querySelector('img[src*=score_]')
const { pathname } = new URL(imgE.src) const { pathname } = new URL(imgE.src)
const imgtype = pathname.match(/\.(\w+)$/)[1] const imgtype = pathname.match(/\.(\w+)$/)[1]
return imgtype as "svg" | "png" return imgtype as 'svg' | 'png'
} catch (_) { } catch (_) {
return null return null
} }
} }
const getTitle = (scorePlayerData: ScorePlayerData) => { const getTitle = (scorePlayerData: ScorePlayerData): string => {
try { try {
return scorePlayerData.json.metadata.title return scorePlayerData.json.metadata.title
} catch (_) { } catch (_) {
return "" return ''
} }
} }
const getScoreFileName = (scorePlayerData: ScorePlayerData) => { const getScoreFileName = (scorePlayerData: ScorePlayerData): string => {
return getTitle(scorePlayerData).replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, "_") return getTitle(scorePlayerData).replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
} }
const fetchMscz = async (url: string): Promise<ArrayBuffer> => { const fetchMscz = async (url: string): Promise<ArrayBuffer> => {
if (!msczBufferP) { if (!msczBufferP) {
msczBufferP = (async () => { msczBufferP = (async (): Promise<ArrayBuffer> => {
const token = await recaptcha.execute() const token = await recaptcha.execute()
const r = await fetch(url + token) const r = await fetch(url + token)
const data = await r.arrayBuffer() const data = await r.arrayBuffer()
return data 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 // init recaptcha
if (!window.UGAPP || !window.UGAPP.store || !window.UGAPP.store.jmuse_settings) { return } // eslint-disable-next-line @typescript-eslint/no-floating-promises
recaptcha.init()
// init recaptcha // @ts-ignore
recaptcha.init() const scorePlayer: ScorePlayerData = window.UGAPP.store.jmuse_settings.score_player
// @ts-ignore const { id } = scorePlayer.json
const scorePlayer: ScorePlayerData = window.UGAPP.store.jmuse_settings.score_player const baseURL = scorePlayer.urls.image_path
const { id } = scorePlayer.json const filename = getScoreFileName(scorePlayer)
const baseURL = scorePlayer.urls.image_path
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 mxlURL = baseURL + 'score.mxl'
const msczURL = `https://musescore.now.sh/api/mscz?id=${id}&token=` const { midi: midiURL, mp3: mp3URL } = scorePlayer.urls
const mxlURL = baseURL + "score.mxl" const btnsDiv = document.querySelector('.score-right .buttons-wrapper') || document.querySelectorAll('aside section > div')[4]
const { midi: midiURL, mp3: mp3URL } = scorePlayer.urls 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] // fix the icon of the download btn
const downloadBtn = btnsDiv.querySelector("button, .button") as HTMLElement // if the `downloadBtn` seleted was a `Print` btn, replace the `print` icon with the `download` icon
downloadBtn.onclick = null 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 const imgType = getImgType() || 'svg'
// if the `downloadBtn` seleted was a `Print` btn, replace the `print` icon with the `download` icon
const svgPath: SVGPathElement = downloadBtn.querySelector("svg > path") const sheetImgURLs = Array.from({ length: getPagesNumber(scorePlayer) }).fill(null).map((_, i) => {
if (svgPath) { return baseURL + `score_${i}.${imgType}`
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 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 textNode = [...btn.childNodes].find((x) => {
return x.textContent.includes('Download') || x.textContent.includes('Print')
const sheetImgURLs = Array.from({ length: getPagesNumber(scorePlayer) }).fill(null).map((_, i) => {
return baseURL + `score_${i}.${imgType}`
}) })
textNode.textContent = `Download ${name}`
const downloadURLs = { return {
"MSCZ": null, btn,
"PDF": null, textNode,
"MusicXML": mxlURL,
"MIDI": midiURL,
"MP3": mp3URL,
"Parts": null,
} }
}
const createBtn = (name: string) => { const newDownloadBtns = Object.keys(downloadURLs).map((name) => {
const btn = downloadBtn.cloneNode(true) as HTMLElement const url = downloadURLs[name]
const { btn, textNode } = createBtn(name)
if (btn.nodeName.toLowerCase() == "button") { if (name === 'PDF') {
btn.setAttribute("style", "width: 205px !important") btn.onclick = async (): Promise<void> => {
} else { const filename = getScoreFileName(scorePlayer)
btn.dataset.target = ""
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<void> => {
textNode.textContent = PROCESSING_TEXT
const textNode = [...btn.childNodes].find((x) => { try {
return x.textContent.includes("Download") || x.textContent.includes("Print") 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<void> => {
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 { // load webmscore (https://github.com/LibreScore/webmscore)
btn, const script = w.document.createElement('script')
textNode, 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<void> => {
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) => { return btn
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)
downloadBtn.replaceWith(...newDownloadBtns)
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
waitForDocumentLoaded().then(main) waitForDocumentLoaded().then(main)

View File

@ -2,12 +2,12 @@
/** /**
* the site key for Google reCAPTCHA v3 * the site key for Google reCAPTCHA v3
*/ */
const SITE_KEY = "6Ldxtt8UAAAAALvcRqWTlVOVIB7MmEWwN-zw_9fM" const SITE_KEY = '6Ldxtt8UAAAAALvcRqWTlVOVIB7MmEWwN-zw_9fM'
type token = string; type token = string;
interface GRecaptcha { interface GRecaptcha {
ready(cb: Function): void; ready (cb: Function): void;
execute(siteKey: string, opts: { action: string }): Promise<token>; execute (siteKey: string, opts: { action: string }): Promise<token>;
} }
let gr: GRecaptcha | Promise<GRecaptcha> let gr: GRecaptcha | Promise<GRecaptcha>
@ -16,33 +16,33 @@ let gr: GRecaptcha | Promise<GRecaptcha>
* load reCAPTCHA * load reCAPTCHA
*/ */
const load = (): Promise<GRecaptcha> => { const load = (): Promise<GRecaptcha> => {
// load script // load script
const script = document.createElement("script") const script = document.createElement('script')
script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${SITE_KEY}` script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${SITE_KEY}`
script.async = true script.async = true
document.body.appendChild(script) document.body.appendChild(script)
// add css // add css
const style = document.createElement("style") const style = document.createElement('style')
style.innerHTML = ".grecaptcha-badge { display: none !important; }" style.innerHTML = '.grecaptcha-badge { display: none !important; }'
document.head.appendChild(style) document.head.appendChild(style)
return new Promise((resolve) => { return new Promise((resolve) => {
script.onload = () => { script.onload = (): void => {
const grecaptcha: GRecaptcha = window["grecaptcha"] const grecaptcha: GRecaptcha = window['grecaptcha']
grecaptcha.ready(() => resolve(grecaptcha)) grecaptcha.ready(() => resolve(grecaptcha))
} }
}) })
} }
export const init = () => { export const init = (): GRecaptcha | Promise<GRecaptcha> => {
if (!gr) { if (!gr) {
gr = load() gr = load()
} }
return gr return gr
} }
export const execute = async (): Promise<token> => { export const execute = async (): Promise<token> => {
const captcha = await init() const captcha = await init()
return captcha.execute(SITE_KEY, { action: "downloadmscz" }) return captcha.execute(SITE_KEY, { action: 'downloadmscz' })
} }

View File

@ -1,84 +1,84 @@
interface SourceData { interface SourceData {
type: string; // "audio" type: string; // "audio"
title: string; // "Musescore audio" title: string; // "Musescore audio"
nid: number; nid: number;
} }
type CommentData = any; type CommentData = any;
interface PartData { interface PartData {
part: { part: {
name: string; name: string;
program: number; program: number;
} };
} }
interface Metadata { interface Metadata {
title: string; title: string;
subtitle?: string; subtitle?: string;
composer?: string; composer?: string;
poet?: string; poet?: string;
pages: number; pages: number;
measures: number; measures: number;
lyrics: number; lyrics: number;
chordnames: number; chordnames: number;
keysig: number; keysig: number;
duration: number; duration: number;
dimensions: number; dimensions: number;
parts: PartData[]; parts: PartData[];
} }
interface ScoreJson { interface ScoreJson {
id: number; id: number;
vid: number; vid: number;
dates: { dates: {
revised: number; revised: number;
}; };
secret: string; secret: string;
permalink: string; permalink: string;
custom_url: string; custom_url: string;
format: string; // "0" format: string; // "0"
has_custom_audio: 0 | 1; has_custom_audio: 0 | 1;
metadata: Metadata; metadata: Metadata;
} }
interface UrlsData { interface UrlsData {
midi: string; midi: string;
mp3: string; mp3: string;
space: string; space: string;
image_path: string; image_path: string;
media?: string[]; media?: string[];
} }
interface AccessControlData { interface AccessControlData {
enabled: boolean; enabled: boolean;
hasAccess: boolean; hasAccess: boolean;
} }
interface PianoKeyboardData extends AccessControlData { interface PianoKeyboardData extends AccessControlData {
midiUrl: string; midiUrl: string;
} }
interface PianoRollData extends AccessControlData { interface PianoRollData extends AccessControlData {
resourcesUrl: string; resourcesUrl: string;
feedbackUrl: string; feedbackUrl: string;
forceShow: boolean; forceShow: boolean;
} }
export interface ScorePlayerData { export interface ScorePlayerData {
embed: boolean; embed: boolean;
sources: SourceData[]; sources: SourceData[];
default_source?: SourceData; default_source?: SourceData;
mixer?: string; mixer?: string;
secondaryMixer?: string; secondaryMixer?: string;
bucket?: string; // "https://musescore.com/static/musescore/scoredata" bucket?: string; // "https://musescore.com/static/musescore/scoredata"
json: ScoreJson; json: ScoreJson;
render_vector: boolean; render_vector: boolean;
comments: CommentData[]; comments: CommentData[];
score_id: number; score_id: number;
urls: UrlsData; urls: UrlsData;
sendEvents?: boolean; sendEvents?: boolean;
pianoKeyboard: PianoKeyboardData; pianoKeyboard: PianoKeyboardData;
pianoRoll: PianoRollData; pianoRoll: PianoRollData;
} }

View File

@ -1,24 +1,24 @@
export const getIndexPath = (id: number) => { export const getIndexPath = (id: number): string => {
const idStr = String(id) 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 // 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 // https://developers.musescore.com/#/file-urls
// "5449062" -> ["2", "6", "0"] // "5449062" -> ["2", "6", "0"]
const indexN = idStr.split("").reverse().slice(0, 3) const indexN = idStr.split('').reverse().slice(0, 3)
return indexN.join("/") return indexN.join('/')
} }
export const waitForDocumentLoaded = (): Promise<void> => { export const waitForDocumentLoaded = (): Promise<void> => {
if (document.readyState !== "complete") { if (document.readyState !== 'complete') {
return new Promise(resolve => { return new Promise(resolve => {
document.addEventListener("readystatechange", () => { document.addEventListener('readystatechange', () => {
if (document.readyState == "complete") { if (document.readyState === 'complete') {
resolve() resolve()
} }
}, { once: true }) }, { once: true })
}) })
} else { } else {
return Promise.resolve() return Promise.resolve()
} }
} }

View File

@ -1,30 +1,30 @@
import { PDFWorkerMessage } from "./worker" import { PDFWorkerMessage } from './worker'
import { PDFWorker } from "../dist/cache/worker" import { PDFWorker } from '../dist/cache/worker'
const scriptUrlFromFunction = (fn: Function) => { const scriptUrlFromFunction = (fn: Function): string => {
const blob = new Blob(["(" + fn.toString() + ")()"], { type: "application/javascript" }) const blob = new Blob(['(' + fn.toString() + ')()'], { type: 'application/javascript' })
return URL.createObjectURL(blob) return URL.createObjectURL(blob)
} }
export class PDFWorkerHelper extends Worker { export class PDFWorkerHelper extends Worker {
constructor() { constructor () {
const url = scriptUrlFromFunction(PDFWorker) const url = scriptUrlFromFunction(PDFWorker)
super(url) super(url)
} }
generatePDF(imgURLs: string[], imgType: "svg" | "png", width: number, height: number): Promise<ArrayBuffer> { generatePDF (imgURLs: string[], imgType: 'svg' | 'png', width: number, height: number): Promise<ArrayBuffer> {
const msg: PDFWorkerMessage = [ const msg: PDFWorkerMessage = [
imgURLs, imgURLs,
imgType, imgType,
width, width,
height, height,
] ]
this.postMessage(msg) this.postMessage(msg)
return new Promise((resolve) => { return new Promise((resolve) => {
this.addEventListener("message", (e) => { this.addEventListener('message', (e) => {
resolve(e.data) resolve(e.data)
}) })
}) })
} }
} }

View File

@ -1,88 +1,87 @@
/// <reference lib="webworker" /> /// <reference lib="webworker" />
import PDFDocument from "pdfkit/lib/document" import PDFDocument from 'pdfkit/lib/document'
import SVGtoPDF from "svg-to-pdfkit" import SVGtoPDF from 'svg-to-pdfkit'
type ImgType = "svg" | "png" type ImgType = 'svg' | 'png'
const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, height: number): Promise<ArrayBuffer> => {
// @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
}
const getDataURL = (blob: Blob): Promise<string> => { const getDataURL = (blob: Blob): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = () => { reader.onload = (): void => {
const result = reader.result const result = reader.result
resolve(result as string) resolve(result as string)
} }
reader.onerror = reject reader.onerror = reject
reader.readAsDataURL(blob) reader.readAsDataURL(blob)
}) })
} }
const fetchDataURL = async (imgUrl: string): Promise<string> => { const fetchDataURL = async (imgUrl: string): Promise<string> => {
const r = await fetch(imgUrl) const r = await fetch(imgUrl)
const blob = await r.blob() const blob = await r.blob()
return getDataURL(blob) return getDataURL(blob)
} }
const fetchText = async (imgUrl: string): Promise<string> => { const fetchText = async (imgUrl: string): Promise<string> => {
const r = await fetch(imgUrl) const r = await fetch(imgUrl)
return r.text() return r.text()
}
const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, height: number): Promise<ArrayBuffer> => {
// @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]; export type PDFWorkerMessage = [string[], ImgType, number, number];
onmessage = async (e) => { onmessage = async (e): Promise<void> => {
const [ const [
imgURLs, imgURLs,
imgType, imgType,
width, width,
height, height,
] = e.data as PDFWorkerMessage ] = e.data as PDFWorkerMessage
const pdfBuf = await generatePDF( const pdfBuf = await generatePDF(
imgURLs, imgURLs,
imgType, imgType,
width, width,
height, height,
) )
postMessage(pdfBuf, [pdfBuf]) postMessage(pdfBuf, [pdfBuf])
} }

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"es2019",
],
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"strictNullChecks": true,
"sourceMap": false,
"newLine": "lf",
}
}