1
0
Fork 0

move export to web worker

This commit is contained in:
Enoch Riese 2022-08-21 10:26:35 +01:00
parent df7011329b
commit 7cf7e0c90a
13 changed files with 248 additions and 133 deletions

View file

@ -55,5 +55,8 @@
},
"browserslist": [
"last 2 versions"
]
],
"engines": {
"node": "14.x"
}
}

View file

@ -1,6 +1,6 @@
import Page from 'site/components/wrappers/page.js'
import useApp from 'site/hooks/useApp.js'
import WorkbenchWrapper from 'shared/components/wrappers/workbench.js'
import WorkbenchWrapper from 'shared/components/wrappers/workbench'
import { useRouter } from 'next/router'
import Layout from 'site/components/layouts/lab'

View file

@ -0,0 +1,56 @@
import yaml from 'js-yaml'
import axios from 'axios'
import PdfExporter from './pdfExporter'
addEventListener('message', async(e) => {
const {format, gist, svg} = e.data
// handle the data exports
if (format === 'json') return exportJson(gist)
if (format === 'yaml') return exportYaml(gist)
if (format === 'github gist') return exportGithubGist(gist)
if (format === 'svg') return exportSvg(gist, svg)
new PdfExporter(e.data).export(postSuccess)
})
const postSuccess = (blob) => {
postMessage({success: true, blob})
close()
}
const exportJson = gist => {
const blob = new Blob([JSON.stringify(gist, null, 2)], {
type: 'application/json;charset=utf-8'
})
postSuccess(blob)
}
const exportYaml = gist => {
const blob = new Blob([yaml.dump(gist)], {
type: 'application/x-yaml;charset=utf-8'
})
postSuccess(blob)
}
const exportSvg = (gist, svg) => {
const blob = new Blob([svg], {
type: 'image/svg+xml;charset=utf-8'
})
postSuccess(blob)
}
const exportGithubGist = (data) => {
axios.post('https://backend.freesewing.org/github/gist', {
design: data.design,
data: yaml.dump(data)
})
.then(res => {
postMessage({
success: true,
link: 'https://gist.github.com/' + res.data.id
})
})
.catch(err => console.log(err))
}

View file

@ -1,13 +1,11 @@
import { useState } from 'react'
import { useTranslation } from 'next-i18next'
import fileSaver from 'file-saver'
import yaml from 'js-yaml'
import axios from 'axios'
import Popout from 'shared/components/popout'
import WebLink from 'shared/components/web-link'
import Worker from 'web-worker';
import fileSaver from 'file-saver'
import theme from '@freesewing/plugin-theme'
import {pagesPlugin} from '../layout/print/plugin'
import PdfExporter from './pdfExporter'
export const exports = {
exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'],
@ -22,28 +20,35 @@ export const defaultPdfSettings = {
coverPage: true
}
export const handleExport = (format, gist, design, t, app, setLink, setFormat) => {
export const handleExport = async(format, gist, design, t, app, onComplete) => {
app.startLoading();
// handle state setting if there's supposed to be any
setLink && setLink(false)
setFormat && setFormat(format)
const worker = new Worker(new URL('./export_worker.js', import.meta.url), {type: module});
// handle the data exports
if (exports.exportAsData.indexOf(format) !== -1) {
if (format === 'json') exportJson(gist)
else if (format === 'yaml') exportYaml(gist)
else if (format === 'github gist') exportGithubGist(gist, app, setLink)
return
worker.addEventListener('message', e => {
if (e.data.blob) {
const fileType = exports.exportForPrinting.indexOf(format) === -1 ? format : 'pdf'
fileSaver.saveAs(e.data.blob, `freesewing-${gist.design || 'gist'}.${fileType}`)
}
app.stopLoading()
onComplete && onComplete(e)
})
gist.embed=false
let svg = ''
// pdf settings
const settings = {
...defaultPdfSettings,
...(gist._state.layout?.forPrinting?.page || {})
}
const workerArgs = {format, gist, settings}
if (exports.exportAsData.indexOf(format) === -1) {
// gist.embed=false
// make a pattern instance for export rendering
const layout = gist.layouts?.printingLayout || gist.layout || true
let pattern = new design({...gist, layout})
// add the theme and translation to the pattern
pattern.use(theme, {stripped: format !== 'svg'})
pattern.use({
@ -52,12 +57,6 @@ export const handleExport = (format, gist, design, t, app, setLink, setFormat) =
}
},{t})
// pdf settings
const settings = {
...defaultPdfSettings,
...(gist._state.layout?.forPrinting?.page || {})
}
// a specified size should override the gist one
if (format !== 'pdf') {
settings.size = format
@ -66,47 +65,28 @@ export const handleExport = (format, gist, design, t, app, setLink, setFormat) =
try {
// add pages to pdf exports
if (format !== 'svg') {
pattern.use(pagesPlugin(settings, true))
pattern.use(pagesPlugin({
...settings,
printStyle: true,
renderBlanks: false,
setPatternSize: true
}))
}
pattern.draft();
svg = pattern.render()
workerArgs.svg = svg
if (pattern.parts.pages) {
workerArgs.pages = pattern.parts.pages.pages
}
} catch(err) {
console.log(err)
app.stopLoading();
}
if (format === 'svg') return exportSvg(gist, svg)
}
return new PdfExporter(gist.design, pattern, svg, settings).export();
}
const exportJson = gist => {
const blob = new Blob([JSON.stringify(gist, null, 2)], {
type: 'application/json;charset=utf-8'
})
fileSaver.saveAs(blob, `freesewing-${gist.design || 'gist'}.json`)
}
const exportYaml = gist => {
const blob = new Blob([yaml.dump(gist)], {
type: 'application/x-yaml;charset=utf-8'
})
fileSaver.saveAs(blob, `freesewing-${gist.design || 'gist'}.yaml`)
}
const exportSvg = (gist, svg) => {
const blob = new Blob([svg], {
type: 'image/svg+xml;charset=utf-8'
})
fileSaver.saveAs(blob, `freesewing-${gist.design || 'pattern'}.svg`)
}
const exportGithubGist = (data, app, setLink) => {
app.setLoading(true)
axios.post('https://backend.freesewing.org/github/gist', {
design: data.design,
data: yaml.dump(data)
})
.then(res => setLink('https://gist.github.com/' + res.data.id))
.catch(err => console.log(err))
.finally(() => app.stopLoading())
worker.postMessage(workerArgs)
}
const ExportDraft = ({ gist, design, app }) => {
@ -115,6 +95,13 @@ const ExportDraft = ({ gist, design, app }) => {
const [format, setFormat] = useState(false)
const { t } = useTranslation(['app'])
const doExport = (format) => {
setLink(false)
setFormat(format)
handleExport(format, gist, design, t, app, (e) => {
if (e.data.link) {setLink(e.data.link)}
})
}
return (
<div className="max-w-screen-xl m-auto">
@ -135,7 +122,7 @@ const ExportDraft = ({ gist, design, app }) => {
{exports[type].map(format => (
<button key={format}
className="btn btn-primary"
onClick={() => handleExport(format, gist, design, t, app, setLink, setFormat)}
onClick={() => doExport(format)}
>
{type === 'exportForPrinting' ? `${format} pdf` : format }
</button>

View file

@ -13,7 +13,8 @@ const pxToPoints = (72/96);
// multiply a mm value by this to get a pixel value
const mmToPx = 3.77953
// multiply a mm value by this to get a points value
const mmToPoints = mmToPx * pxToPoints
// const mmToPoints = mmToPx * pxToPoints
const mmToPoints = 2.834645669291339
/**
* Freesewing's first explicit class?
@ -30,6 +31,7 @@ export default class Exporter {
pattern
/** the pdfKit instance that is writing the document */
pdf
buffers
/** the usable width (excluding margin) of the pdf page, in points */
pageWidth
@ -51,36 +53,40 @@ export default class Exporter {
pagesWithContent = {}
constructor(designName, pattern, svg, settings) {
// default just in case
this.designName = designName || 'freesewing'
constructor({svg, settings, pages}) {
this.settings = settings
this.pattern = pattern
this.pagesWithContent = pages.withContent;
this.svg = svg
this.createPdf()
this.margin = this.settings.margin * mmToPoints // margin is in mm because it comes from us, so we convert it to points
this.pageHeight = this.pdf.page.height - this.margin // this is in points because it comes from pdfKit
this.pageWidth = this.pdf.page.width - this.margin// this is in points because it comes from pdfKit
this.pageHeight = this.pdf.page.height - this.margin * 2 // this is in points because it comes from pdfKit
this.pageWidth = this.pdf.page.width - this.margin * 2// this is in points because it comes from pdfKit
// console.log(pages.width * pages.cols, pages.height * pages.rows, this.pageWidth, this.pageHeight)
// console.log(svg)
// we pass the svg as a string, so we need to make it a DOM element so we can manipulate it
const divElem = document.createElement('div');
divElem.innerHTML = svg;
this.svg = divElem.firstElementChild;
// const divElem = document.createElement('div');
// divElem.innerHTML = svg;
// this.svg = divElem.firstElementChild;
// get the pages data
const pages = this.pattern.parts.pages.pages
this.columns = pages.cols
this.rows = pages.rows
// then set the svg's width and height in points to include all pages in full (the original svg will have been set to show only as much page as is occupied)
this.svgWidth = this.columns * this.pageWidth
this.svgHeight = this.rows * this.pageHeight
this.svg.setAttribute('height', this.svgWidth + 'pt')
this.svg.setAttribute('width', this.svgHeight + 'pt')
this.svgWidth = this.columns * pages.width * mmToPoints
this.svgHeight = this.rows * pages.height * mmToPoints
console.log(this.columns, this.rows, pages.width, pages.height, this.svgWidth, this.svgHeight)
console.log(svg)
// this.svg.replace(/width="\d+mm"/, `width="${this.svgWidth}pt"`)
// this.svg.replace(/height="\d+mm"/, `height="${this.svgHeight}pt"`)
// this.svg.setAttribute('height', this.svgWidth + 'pt')
// this.svg.setAttribute('width', this.svgHeight + 'pt')
// set the viewbox to include all pages in full as well, this time in mm
this.svg.setAttribute('viewBox', `0 0 ${this.columns * this.pageWidthInMm} ${this.rows * this.pageHeightInMm}`)
// this.svg.setAttribute('viewBox', `0 0 ${this.columns * this.pageWidthInMm} ${this.rows * this.pageHeightInMm}`)
}
/** pdf page usable (excluding margin) width, in mm */
@ -99,18 +105,9 @@ export default class Exporter {
// PdfKit wants to flush the buffer on each new page.
// We don't want to try to save the document until it's complete, so we have to manage the buffers ourselves
const buffers = [];
this.buffers = [];
// add new data to our buffer storage
this.pdf.on('data', buffers.push.bind(buffers));
// when the end event fires, then we save the whole thing
this.pdf.on('end', () => {
// convert buffers to a blob
const blob = new Blob(buffers, {
type: 'application/pdf'
})
// save the blob
fileSaver.saveAs(blob, `freesewing-${this.designName}.pdf`)
});
this.pdf.on('data', this.buffers.push.bind(this.buffers));
}
/**
@ -197,8 +194,16 @@ export default class Exporter {
}
/** export to pdf */
async export() {
this.scanPages()
async export(onComplete) {
// when the end event fires, then we save the whole thing
this.pdf.on('end', () => {
// convert buffers to a blob
const blob = new Blob(this.buffers, {
type: 'application/pdf'
})
onComplete(blob)
});
await this.generateCoverPage()
await this.generatePages();
this.save()
@ -218,7 +223,7 @@ export default class Exporter {
let coverWidth = this.pageWidth - coverMargin * 2
// add the entire pdf to the page, so that it fills the available space as best it can
await SVGtoPDF(this.pdf, this.svg.outerHTML, coverMargin, coverMargin, {
await SVGtoPDF(this.pdf, this.svg, coverMargin, coverMargin, {
width: coverWidth,
height: coverHeight,
assumePt: true,
@ -234,11 +239,12 @@ export default class Exporter {
assumePt: true,
width: this.svgWidth,
height: this.svgHeight,
preserveAspectRatio: 'xMinYMin slice'
preserveAspectRatio: 'xMinYMin slice',
useCSS: true
}
// everything is offset by half a margin so that it's centered on the page
const startMargin = 0.5 * this.margin
const startMargin = this.margin
for (var h = 0; h < this.rows; h++) {
for (var w = 0; w < this.columns; w++) {
// skip empty pages
@ -255,7 +261,7 @@ export default class Exporter {
}
// add the pdf to the page, offset by the page distances
await SVGtoPDF(this.pdf, this.svg.outerHTML, x, y, options)
await SVGtoPDF(this.pdf, this.svg, x, y, options)
}
}
}

View file

@ -7,6 +7,8 @@ import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"
const Draft = props => {
const { draft, patternProps, gist, updateGist, app, bgProps={}, fitLayoutPart = false, layoutType="printingLayout"} = props
const svgRef = useRef(null);
if (!patternProps) return null
// keep a fresh copy of the layout because we might manipulate it without saving to the gist
let layout = draft.settings.layout === true ? {
...patternProps.autoLayout,
@ -14,9 +16,7 @@ const Draft = props => {
height: patternProps.height
} : {...draft.settings.layout}
const svgRef = useRef(null);
if (!patternProps) return null
// Helper method to update part layout and re-calculate width * height
const updateLayout = (name, config, history=true) => {

View file

@ -35,7 +35,7 @@ const PrintLayout = props => {
const bgProps = { fill: "url(#page)" }
const exportIt = () => {
handleExport('pdf', props.gist, props.design, t)
handleExport('pdf', props.gist, props.design, t, props.app)
}
return (

View file

@ -16,15 +16,55 @@ const indexLetter = (i) => String.fromCharCode('A'.charCodeAt(0) + i - 1)
/**
* A plugin to add printer pages
* */
export const pagesPlugin = ({size='a4', orientation='portrait', margin=10}, printStyle = false /** should the pages be rendered for printing or for screen viewing? */ ) => {
export const pagesPlugin = ({
size='a4',
orientation='portrait',
margin=10,
printStyle = false, /** should the pages be rendered for printing or for screen viewing? */
setPatternSize = true
}) => {
const ls = orientation === 'landscape'
let sheetHeight = sizes[size][ls ? 1 : 0]
let sheetWidth = sizes[size][ls ? 0 : 1]
sheetWidth -= margin
sheetHeight -= margin
return basePlugin({sheetWidth, sheetHeight, orientation, printStyle})
let sheetHeight = sizes[size][ls ? 0 : 1]
let sheetWidth = sizes[size][ls ? 1 : 0]
sheetWidth -= margin * 2
sheetHeight -= margin * 2
return basePlugin({sheetWidth, sheetHeight, orientation, printStyle, setPatternSize})
}
const doScanForBlanks = (parts, layout, x, y, w, h) => {
let hasContent = false
for (var p in parts) {
let part = parts[p]
// skip the pages part and any that aren't rendered
if (part === this || part.render === false || part.isEmpty()) continue
// get the position of the part
let partLayout = layout.parts[p]
let partMinX = (partLayout.tl?.x || (partLayout.move.x + part.topLeft.x))
let partMinY = (partLayout.tl?.y || (partLayout.move.y + part.topLeft.y))
let partMaxX = (partLayout.br?.x || (partMinX + part.width))
let partMaxY = (partLayout.br?.y || (partMinY + part.height))
// check if the part overlaps the page extents
if (
// if the left of the part is further left than the right end of the page
partMinX < x + w &&
// and the top of the part is above the bottom of the page
partMinY < y + h &&
// and the right of the part is further right than the left of the page
partMaxX > x &&
// and the bottom of the part is below the top to the page
partMaxY > y
) {
// the part has content inside the page
hasContent = true;
// so we stop looking
break;
}
}
return hasContent
}
/**
* The base plugin for adding a layout helper part like pages or fabric
* sheetWidth: the width of the helper part
@ -39,7 +79,10 @@ const basePlugin = ({
boundary=false,
partName="pages",
responsiveColumns=true,
printStyle=false
printStyle=false,
scanForBlanks=true,
renderBlanks=true,
setPatternSize=false
}) => ({
name,
version,
@ -62,9 +105,13 @@ const basePlugin = ({
responsiveColumns && (width += pattern.settings.layout.topLeft.x)
}
macro('addPages', { size: [sheetWidth, sheetHeight], height, width })
macro('addPages', { size: [sheetHeight,sheetWidth, ], height, width })
if (boundary) pattern.parts[partName].boundary();
if (setPatternSize) {
pattern.width = sheetWidth * pattern.parts[partName].pages.cols
pattern.height = sheetHeight * pattern.parts[partName].pages.rows
}
}
},
macros: {
@ -74,13 +121,22 @@ const basePlugin = ({
const cols = Math.ceil(so.width / w)
const rows = Math.ceil(so.height / h)
const { points, Point, paths, Path, macro } = this.shorthand()
let x = 0
let y = 0
let count = 0
let withContent = {}
// get the layout from the pattern
const layout = typeof this.context.settings.layout === 'object' ? this.context.settings.layout : this.context.autoLayout;
for (let row=0;row<rows;row++) {
x=0
let y = row * h
withContent[row] = {}
for (let col=0;col<cols;col++) {
count++
let x = col * w
let hasContent = true
if (scanForBlanks && layout) {
hasContent = doScanForBlanks(this.context.parts, layout, x, y, w, h)
withContent[row][col] = hasContent
if (!renderBlanks && !hasContent) continue
}
if (hasContent) count++
const pageName = `_pages__row${row}-col${col}`
points[`${pageName}-tl`] = new Point(x,y)
points[`${pageName}-tr`] = new Point(x+w,y)
@ -114,12 +170,10 @@ const basePlugin = ({
macro('addRuler', {xAxis: true, pageName})
macro('addRuler', {xAxis: false, pageName})
}
x += w
}
y += h
}
// Store page count in part
this.pages = { cols, rows, count: cols*rows }
this.pages = { cols, rows, count, withContent, width: w, height: h}
},
/** add a ruler to the top left corner of the page */

View file

@ -49,7 +49,7 @@ const PrintLayoutSettings = props => {
<input
type="range"
max={50}
min={5}
min={0}
step={1}
onChange={setMargin}
value={margin}

View file

@ -11,7 +11,7 @@ import Modal from 'shared/components/modal'
import Measurements from 'shared/components/workbench/measurements/index.js'
import LabDraft from 'shared/components/workbench/draft/index.js'
import LabSample from 'shared/components/workbench/sample.js'
import ExportDraft from 'shared/components/workbench/exporting/index.js'
import ExportDraft from 'shared/components/workbench/exporting/index'
import GistAsJson from 'shared/components/workbench/json.js'
import GistAsYaml from 'shared/components/workbench/yaml.js'
import DraftEvents from 'shared/components/workbench/events.js'

View file

@ -39,7 +39,8 @@
"remark-smartypants": "^2.0.0",
"svg-to-pdfkit": "^0.1.8",
"to-vfile": "^7.2.2",
"unist-util-visit": "^4.1.0"
"unist-util-visit": "^4.1.0",
"web-worker": "^1.2.0"
},
"devDependencies": {
"autoprefixer": "^10.4.0",
@ -47,5 +48,8 @@
"postcss": "^8.4.4",
"tailwindcss": "^3.0.1",
"tailwindcss-open-variant": "^1.0.0"
},
"engines": {
"node": "14.x"
}
}

View file

@ -33,7 +33,7 @@ const loadFromUnpkg = (design, version) => {
const pageTemplate = design => `${header}
import design from 'designs/${design}/src/index.js'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import PageTemplate from 'site/page-templates/workbench.js'
import PageTemplate from 'site/page-templates/workbench'
const Page = (props) => <PageTemplate {...props} design={design} version="next"/>
export default Page

View file

@ -24378,6 +24378,11 @@ web-streams-polyfill@^3.0.3:
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
web-worker@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da"
integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"