From cd7c6eb5c537cdb1ece8708fbd5a16cb1deace80 Mon Sep 17 00:00:00 2001 From: Michael Fix Date: Sat, 21 Nov 2020 16:42:09 -0800 Subject: [PATCH] Clean up getCarbonImage (#1126) * clean up getCarbonImage * remove createObjectURL change * bug fixes * refactor getCarbonImage to use blobs/dataURLs only * add console.error for copy to clipboard failures * use blob.type for ClipboardItem * add TODO --- components/Editor.js | 140 +++++++++++++++++++------------------------ lib/util.js | 12 ++++ 2 files changed, 75 insertions(+), 77 deletions(-) diff --git a/components/Editor.js b/components/Editor.js index 1253f5b..8266083 100644 --- a/components/Editor.js +++ b/components/Editor.js @@ -33,7 +33,7 @@ import { FONTS, } from '../lib/constants' import { serializeState, getRouteState } from '../lib/routing' -import { getSettings, unescapeHtml, formatCode, omit } from '../lib/util' +import { getSettings, unescapeHtml, formatCode, omit, dataURLtoBlob } from '../lib/util' import domtoimage from '../lib/dom-to-image' const languageIcon = @@ -110,32 +110,8 @@ class Editor extends React.Component { exportSize = (EXPORT_SIZES_HASH[this.state.exportSize] || DEFAULT_EXPORT_SIZE).value, } = { format: 'png' } ) => { - // if safari, get image from api - const isPNG = format !== 'svg' - if (this.context.image && this.isSafari && isPNG) { - const themeConfig = this.getTheme() - // pull from custom theme highlights, or state highlights - const encodedState = serializeState({ - ...this.state, - highlights: { ...themeConfig.highlights, ...this.state.highlights }, - }) - return this.context.image(encodedState) - } - const node = this.carbonNode.current - // const map = new Map() - // if (isPNG) { - // node.querySelectorAll('span[role="presentation"]').forEach(node => { - // if (node.innerText && node.innerText.match(/%[A-Fa-f0-9]{2}/)) { - // map.set(node, node.innerHTML) - // node.innerText.match(/%[A-Fa-f0-9]{2}/g).forEach(t => { - // node.innerHTML = node.innerHTML.replace(t, encodeURIComponent(t)) - // }) - // } - // }) - // } - const width = node.offsetWidth * exportSize const height = squared ? node.offsetWidth * exportSize : node.offsetHeight * exportSize @@ -163,43 +139,49 @@ class Editor extends React.Component { // current font-family used const fontFamily = this.state.fontFamily - try { - // TODO consolidate type/format to only use one param - if (type === 'objectURL') { - if (format === 'svg') { - return domtoimage - .toSvg(node, config) - .then(dataUrl => - dataUrl - .replace(/ /g, ' ') - // https://github.com/tsayen/dom-to-image/blob/fae625bce0970b3a039671ea7f338d05ecb3d0e8/src/dom-to-image.js#L551 - .replace(/%23/g, '#') - .replace(/%0A/g, '\n') - // https://stackoverflow.com/questions/7604436/xmlparseentityref-no-name-warnings-while-loading-xml-into-a-php-file - .replace(/&(?!#?[a-z0-9]+;)/g, '&') - // remove other fonts which are not used - .replace( - new RegExp('@font-face\\s+{\\s+font-family: (?!"*' + fontFamily + ').*?}', 'g'), - '' - ) - ) - .then(uri => uri.slice(uri.indexOf(',') + 1)) - .then(data => new Blob([data], { type: 'image/svg+xml' })) - .then(data => window.URL.createObjectURL(data)) - } - return await domtoimage.toBlob(node, config).then(blob => window.URL.createObjectURL(blob)) - } + // TODO consolidate type/format to only use one param + if (format === 'svg') { + return domtoimage + .toSvg(node, config) + .then(dataURL => + dataURL + .replace(/ /g, ' ') + // https://github.com/tsayen/dom-to-image/blob/fae625bce0970b3a039671ea7f338d05ecb3d0e8/src/dom-to-image.js#L551 + .replace(/%23/g, '#') + .replace(/%0A/g, '\n') + // https://stackoverflow.com/questions/7604436/xmlparseentityref-no-name-warnings-while-loading-xml-into-a-php-file + .replace(/&(?!#?[a-z0-9]+;)/g, '&') + // remove other fonts which are not used + .replace( + new RegExp('@font-face\\s+{\\s+font-family: (?!"*' + fontFamily + ').*?}', 'g'), + '' + ) + ) + .then(uri => uri.slice(uri.indexOf(',') + 1)) + .then(data => new Blob([data], { type: 'image/svg+xml' })) + } - if (type === 'blob') { - return await domtoimage.toBlob(node, config) - } + // if safari, get image from api + if (this.context.image && this.isSafari) { + const themeConfig = this.getTheme() + // pull from custom theme highlights, or state highlights + const encodedState = serializeState({ + ...this.state, + highlights: { ...themeConfig.highlights, ...this.state.highlights }, + }) + // TODO consider returning blob responseType from axios + return this.context + .image(encodedState) + .then(dataURL => (type === 'blob' ? dataURLtoBlob(dataURL) : dataURL)) + } - // Twitter needs regular dataurls - return await domtoimage.toPng(node, config) - } finally { - // map.forEach((value, node) => (node.innerHTML = value)) + if (type === 'blob') { + return domtoimage.toBlob(node, config) } + + // Twitter needs regular dataURLs + return domtoimage.toPng(node, config) } tweet = () => { @@ -213,28 +195,32 @@ class Editor extends React.Component { const prefix = options.filename || this.state.name || 'carbon' - return this.getCarbonImage({ format, type: 'objectURL' }).then(url => { - if (format !== 'open') { - link.download = `${prefix}.${format}` - } - if (this.isFirefox) { - link.target = '_blank' - } - link.href = url - document.body.appendChild(link) - link.click() - link.remove() - }) + return this.getCarbonImage({ format, type: 'blob' }) + .then(blob => window.URL.createObjectURL(blob)) + .then(url => { + if (format !== 'open') { + link.download = `${prefix}.${format}` + } + if (this.isFirefox) { + link.target = '_blank' + } + link.href = url + document.body.appendChild(link) + link.click() + link.remove() + }) } copyImage = () => - this.getCarbonImage({ format: 'png', type: 'blob' }).then(blob => - navigator.clipboard.write([ - new window.ClipboardItem({ - 'image/png': blob, - }), - ]) - ) + this.getCarbonImage({ format: 'png', type: 'blob' }) + .then(blob => + navigator.clipboard.write([ + new window.ClipboardItem({ + [blob.type]: blob, + }), + ]) + ) + .catch(console.error) updateSetting = (key, value) => { this.updateState({ [key]: value }) diff --git a/lib/util.js b/lib/util.js index c89d00d..b474d0d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -94,3 +94,15 @@ export const formatCode = async code => { export const stringifyRGBA = obj => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})` export const generateId = () => Math.random().toString(36).slice(2) + +export function dataURLtoBlob(dataurl) { + const [first, second] = dataurl.split(',') + const mime = first.match(/:(.*?);/)[1] + const bstr = atob(second) + let n = bstr.length + const u8arr = new Uint8Array(n) + while (n--) { + u8arr[n] = bstr.charCodeAt(n) + } + return new Blob([u8arr], { type: mime }) +}