[fix] export from other pages, export sizes (#525)

* support larger files, reparent shapes correctly

* serialize GIFs
This commit is contained in:
Steve Ruiz 2022-01-19 12:33:57 +00:00 committed by GitHub
parent 30d1d7721c
commit 16e8fe1426
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 163 additions and 93 deletions

View file

@ -29,7 +29,7 @@
"@tldraw/tldraw": "^1.5.2",
"@types/next-auth": "^3.15.0",
"aws-sdk": "^2.1053.0",
"chrome-aws-lambda": "^9.0.0",
"chrome-aws-lambda": "9.0.0",
"next": "^12.0.7",
"next-auth": "^4.0.5",
"next-pwa": "^5.4.4",
@ -46,7 +46,8 @@
"cors": "^2.8.5",
"eslint": "7.32.0",
"eslint-config-next": "11.1.2",
"puppeteer": "^13.1.1",
"typescript": "^4.5.2"
},
"gitHead": "838fabdbff1a66d4d7ee8aa5c5d117bc55acbff2"
}
}

View file

@ -2,13 +2,16 @@ import { NextApiRequest, NextApiResponse } from 'next'
import chromium from 'chrome-aws-lambda'
import Cors from 'cors'
import { TDExport, TDExportTypes, TldrawApp } from '@tldraw/tldraw'
import { AnyLengthString } from 'aws-sdk/clients/comprehend'
const cors = Cors({
methods: ['POST'],
})
function runMiddleware(req, res, fn) {
function runMiddleware(
req: NextApiRequest,
res: NextApiResponse,
fn: (req: NextApiRequest, res: NextApiResponse, fn: (args: any) => any) => any
) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) return reject(result)
@ -49,16 +52,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await page.setUserAgent(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
)
await page.setViewport({ width: Math.floor(width), height: Math.floor(height) })
await page.goto(FRONTEND_URL, { timeout: 15 * 1000, waitUntil: 'networkidle0' })
await page.setViewport({ width: Math.floor(width), height: Math.floor(height) })
await page.evaluateHandle('document.fonts.ready')
let err: AnyLengthString
let err: string
await page.evaluate(async (body: TDExport) => {
try {
let app = window.app
if (!app) app = await new Promise((resolve) => setTimeout(() => resolve(window.app), 250))
await app.ready
const { assets, shapes } = body
const { assets, shapes, currentPageId } = body
// If the hapes were a direct child of their current page,
// reparent them to the app's current page.
shapes.forEach((shape) => {
if (shape.parentId === currentPageId) {
shape.parentId = app.currentPageId
}
})
app.patchAssets(assets)
app.createShapes(...shapes)
app.selectAll()
@ -81,3 +91,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.status(500).send(err)
}
}
// Allow the server to support requests with up to 5mb of data.
export const config = {
api: {
bodyParser: {
sizeLimit: '5mb',
},
},
}

View file

@ -1,4 +1,4 @@
import { TDExport, TDExportTypes } from '@tldraw/tldraw'
import { TDExport } from '@tldraw/tldraw'
export const EXPORT_ENDPOINT =
process.env.NODE_ENV === 'development'

View file

@ -446,12 +446,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
})
}
// Cleanup assets
if (!('assets' in next.document)) next.document.assets = {}
Object.keys(next.document.assets).forEach((id) => {
if (!next.document.assets[id]) {
delete next.document.assets[id]
Object.keys(next.document.assets ?? {}).forEach((id) => {
if (!next.document.assets?.[id]) {
delete next.document.assets?.[id]
}
})
@ -3430,12 +3427,28 @@ export class TldrawApp extends StateManager<TDSnapshot> {
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const canvasContext = canvas.getContext('2d')!
canvasContext.drawImage(video, 0, 0)
canvas.getContext('2d')!.drawImage(video, 0, 0)
return canvas.toDataURL('image/png')
} else throw new Error('Video with id ' + id + ' not found')
}
/**
* Get a snapshot of a image (e.g. a GIF) as base64 encoded image
* @param id ID of image shape
* @returns base64 encoded frame
* @throws Error if image shape with given ID does not exist
*/
serializeImage(id: string): string {
const image = document.getElementById(id + '_image') as HTMLImageElement
if (image) {
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
canvas.getContext('2d')!.drawImage(image, 0, 0)
return canvas.toDataURL('image/png')
} else throw new Error('Image with id ' + id + ' not found')
}
patchAssets(assets: TDAssets) {
this.document.assets = {
...this.document.assets,
@ -3458,55 +3471,62 @@ export class TldrawApp extends StateManager<TDSnapshot> {
}
async exportShapesAs(shapeIds: string[], size: number[], type: TDExportTypes) {
if (!this.callbacks.onExport) return
this.setIsLoading(true)
const assets: TDAssets = {}
let shapes = shapeIds.map((id) => ({ ...this.getShape(id) }))
// Patch asset table. Replace videos with serialized snapshots
shapes.forEach((s, i) => {
if (s.assetId) {
assets[s.assetId] = { ...this.document.assets[s.assetId] }
if (s.type === TDShapeType.Video) {
assets[s.assetId].src = this.serializeVideo(s.id)
assets[s.assetId].type = TDAssetType.Image
try {
const assets: TDAssets = {}
const shapes: TDShape[] = shapeIds.map((id) => {
const shape = { ...this.getShape(id) }
if (shape.assetId) {
const asset = { ...this.document.assets[shape.assetId] }
// If the asset is a GIF, then serialize an image
if (asset.src.toLowerCase().endsWith('gif')) {
asset.src = this.serializeImage(shape.id)
}
// If the asset is an image, then serialize an image
if (shape.type === TDShapeType.Video) {
asset.src = this.serializeVideo(shape.id)
asset.type = TDAssetType.Image
// Cast shape to image shapes to properly display snapshots
;(shape as unknown as ImageShape).type = TDShapeType.Image
}
// Patch asset table
assets[shape.assetId] = asset
}
return shape
})
// Create serialized data for JSON or SVGs
let serialized: string | undefined
if (type === TDExportTypes.SVG) {
serialized = this.copySvg(shapeIds)
} else if (type === TDExportTypes.JSON) {
serialized = this.copyJson(shapeIds)
}
})
// Cast exported video shapes to image shapes to properly display snapshots
shapes = shapes.map((s) => {
if (s.type === TDShapeType.Video) {
const shape = s as TDShape
shape.type = TDShapeType.Image
return shape as ImageShape
} else return s
})
let serializedExport
if (type == TDExportTypes.SVG) {
serializedExport = this.copySvg(shapeIds)
} else if (type == TDExportTypes.JSON) {
serializedExport = this.copyJson(shapeIds)
}
const exportInfo: TDExport = {
name: this.page.name ?? 'export',
shapes: shapes,
assets: assets,
type,
size: type === 'png' ? Vec.mul(size, 2) : size,
serialized: serializedExport,
}
if (this.callbacks.onExport) {
try {
this.setIsLoading(true)
await this.callbacks.onExport?.(exportInfo)
} catch (error) {
console.error(error)
} finally {
this.setIsLoading(false)
const exportInfo: TDExport = {
currentPageId: this.currentPageId,
name: this.page.name ?? 'export',
shapes,
assets,
type,
serialized,
size: type === 'png' ? Vec.mul(size, 2) : size,
}
await this.callbacks.onExport(exportInfo)
} catch (error) {
console.error(error)
} finally {
this.setIsLoading(false)
}
}

View file

@ -80,6 +80,7 @@ export class ImageUtil extends TDShapeUtil<T, E> {
isGhost={isGhost}
>
<ImageElement
id={shape.id + '_image'}
ref={rImage}
src={(asset as TDImageAsset).src}
alt="tl_image_asset"

View file

@ -500,6 +500,7 @@ export enum TDExportTypes {
}
export interface TDExport {
currentPageId: string
name: string
shapes: TDShape[]
assets: TDAssets

View file

@ -5278,10 +5278,10 @@ chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.4:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chrome-aws-lambda@^9.0.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/chrome-aws-lambda/-/chrome-aws-lambda-9.1.0.tgz#f3a426fa6588f2fa9452ff62d60afaa2103b2c04"
integrity sha512-y6MOarSzL5LmDv8sWUdDe4wHuAAEeWMlCbLwmL6Bj+pBPava37UuEPbWD5YAXyJx6oHoC0efLGqYDi56k4KMUQ==
chrome-aws-lambda@9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/chrome-aws-lambda/-/chrome-aws-lambda-9.0.0.tgz#4a727d1ab38c691912c80a5c552c1667f43c6bad"
integrity sha512-9+ZgDNAHlQ18pJL2QpBbPK7Knad/a23IaNYQO3AuJQsjW/XKGQdt8mgyKpP85x0iD3WN1ZDdMgibumCz27jT7Q==
dependencies:
lambdafs "^2.0.3"
@ -6438,6 +6438,11 @@ devtools-protocol@0.0.869402:
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.869402.tgz#03ade701761742e43ae4de5dc188bcd80f156d8d"
integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==
devtools-protocol@0.0.948846:
version "0.0.948846"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.948846.tgz#bff47e2d1dba060130fa40ed2e5f78b916ba285f"
integrity sha512-5fGyt9xmMqUl2VI7+rnUkKCiAQIpLns8sfQtTENy5L70ktbNw0Z3TFJ1JoFNYdx/jffz4YXU45VF75wKZD7sZQ==
dezalgo@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
@ -7462,17 +7467,7 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-zip@^1.0.3:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
extract-zip@^2.0.0, extract-zip@^2.0.1:
extract-zip@2.0.1, extract-zip@^2.0.0, extract-zip@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@ -7483,6 +7478,16 @@ extract-zip@^2.0.0, extract-zip@^2.0.1:
optionalDependencies:
"@types/yauzl" "^2.9.1"
extract-zip@^1.0.3:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@ -8583,6 +8588,14 @@ https-browserify@1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
https-proxy-agent@^2.2.3:
version "2.2.4"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
@ -8591,14 +8604,6 @@ https-proxy-agent@^2.2.3:
agent-base "^4.3.0"
debug "^3.1.0"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@ -12108,6 +12113,13 @@ pirates@^4.0.4:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.4.tgz#07df81e61028e402735cdd49db701e4885b4e6e6"
integrity sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw==
pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
pkg-dir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
@ -12115,13 +12127,6 @@ pkg-dir@^3.0.0:
dependencies:
find-up "^3.0.0"
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
platform@1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
@ -12319,7 +12324,7 @@ process@0.11.10, process@^0.11.10:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
progress@^2.0.0, progress@^2.0.1, progress@^2.0.3:
progress@2.0.3, progress@^2.0.0, progress@^2.0.1, progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@ -12411,7 +12416,7 @@ protoduck@^5.0.1:
dependencies:
genfun "^5.0.0"
proxy-from-env@^1.1.0:
proxy-from-env@1.1.0, proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
@ -12493,6 +12498,24 @@ puppeteer-core@^9.0.0:
unbzip2-stream "^1.3.3"
ws "^7.2.3"
puppeteer@^13.1.1:
version "13.1.1"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-13.1.1.tgz#63771eb744202aa91918c49123f846e1747da121"
integrity sha512-GwdFy1JQ43Hhxj6MraXme+XfCX2CKe18MuwToXTMEAk0txg6vUEgwqBnzErTTqDVZ7sWYrDtDaRCfD2y7ZwgGw==
dependencies:
debug "4.3.2"
devtools-protocol "0.0.948846"
extract-zip "2.0.1"
https-proxy-agent "5.0.0"
node-fetch "2.6.5"
pkg-dir "4.2.0"
progress "2.0.3"
proxy-from-env "1.1.0"
rimraf "3.0.2"
tar-fs "2.1.1"
unbzip2-stream "1.4.3"
ws "8.2.3"
q@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -14196,7 +14219,7 @@ tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
tar-fs@^2.0.0, tar-fs@^2.1.1:
tar-fs@2.1.1, tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
@ -14776,7 +14799,7 @@ unbox-primitive@^1.0.1:
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
unbzip2-stream@^1.0.9, unbzip2-stream@^1.3.3:
unbzip2-stream@1.4.3, unbzip2-stream@^1.0.9, unbzip2-stream@^1.3.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
@ -15570,6 +15593,11 @@ write-pkg@^3.1.0:
sort-keys "^2.0.0"
write-json-file "^2.2.0"
ws@8.2.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
ws@>=7.4.6:
version "8.4.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6"