Skip to content

Commit

Permalink
feat(app/dom-export) reduction of svg export size (#433)
Browse files Browse the repository at this point in the history
* chore: update commitlint.config.js
* feat(dom-export): merge html to image updates
* feat(dom-export): add experimental optimize fonts behavior
* test(dom-export): add embed font test dom export
* feat(app): enable `experimental_optimizeFontLoading` for svg export
* feat(dom-export): reduce svg export size
* fix(dom-export): fix sandbox clone for web components
* fix(dom-export): fix web components default styles
  • Loading branch information
riccardoperra authored Jan 1, 2023
1 parent 6fd689d commit 25c4fdf
Show file tree
Hide file tree
Showing 22 changed files with 992 additions and 260 deletions.
5 changes: 5 additions & 0 deletions .changeset/purple-bugs-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codeimage/dom-export': minor
---

Add experimental `experimental_optimizeFontLoading` property
5 changes: 5 additions & 0 deletions .changeset/shiny-nails-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codeimage/app': patch
---

Enable `experimental_optimizeFontLoading` for svg export
3 changes: 2 additions & 1 deletion apps/codeimage/src/components/Toolbar/ExportNewTabButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useI18n} from '@codeimage/locale';
import {Button, toast} from '@codeimage/ui';
import {getUmami} from '@core/constants/umami';
import {useModality} from '@core/hooks/isMobile';
import {Component, createEffect, untrack} from 'solid-js';
import {
Expand All @@ -25,7 +26,7 @@ export const ExportInNewTabButton: Component<ExportButtonProps> = props => {
data.loading ? t('toolbar.loadingNewTab') : t('toolbar.openNewTab');

function openInTab() {
umami.trackEvent(`true`, 'export-new-tab');
getUmami().trackEvent(`true`, 'export-new-tab');

notify({
ref: props.canvasRef,
Expand Down
1 change: 1 addition & 0 deletions apps/codeimage/src/hooks/use-export-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export async function exportImage(
},
pixelRatio: pixelRatio,
quality: quality,
experimental_optimizeFontLoading: true,
};

async function exportByMode(ref: HTMLElement) {
Expand Down
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
'highlight',
'config',
'locale',
'dom-export',
// Only using changeset
'changeset',
// Must be used for ci only or deploy commit
Expand Down
5 changes: 4 additions & 1 deletion packages/dom-export/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"scripts": {
"lint": "eslint . --ext .js,.jsx,.mjs,.ts,.tsx -c ../../.eslintrc.js",
"build": "vite build",
"test": "vitest",
"typecheck:ci": "tsc --skipLibCheck --project tsconfig.dts.json"
},
"lint-staged": {
Expand Down Expand Up @@ -54,13 +55,15 @@
],
"devDependencies": {
"@types/node": "^18.11.0",
"happy-dom": "8.1.1",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"tslib": "^2.4.0",
"typescript": "^4.8.4",
"vite": "^3.1.8",
"vite-plugin-dts": "^1.6.6"
"vite-plugin-dts": "^1.6.6",
"vitest": "0.26.2"
},
"repository": {
"type": "git",
Expand Down
29 changes: 0 additions & 29 deletions packages/dom-export/src/lib/applyStyleWithOptions.ts

This file was deleted.

127 changes: 88 additions & 39 deletions packages/dom-export/src/lib/cloneNode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {isIOS} from '@solid-primitives/platform';
import {clonePseudoElements} from './clonePseudoElements';
import {copyFont, copyUserComputedStyleFast} from './cloneStyle';
import {getBlobFromURL} from './getBlobFromURL';
import {Options} from './options';
import {createImage, getMimeType, makeDataUrl, toArray} from './util';
import {isIOS} from '@solid-primitives/platform';

async function cloneCanvasElement(canvas: HTMLCanvasElement) {
const dataURL = canvas.toDataURL();
Expand Down Expand Up @@ -35,7 +36,7 @@ async function cloneSingleNode<T extends HTMLElement>(
return cloneVideoElement(node, options);
}

return Promise.resolve(node.cloneNode(false) as T);
return node.cloneNode(false) as T;
}

const isSlotElement = (node: HTMLElement): node is HTMLSlotElement =>
Expand All @@ -55,43 +56,39 @@ async function cloneChildren<T extends HTMLElement>(
return Promise.resolve(clonedNode);
}

return children
.reduce(
(deferred, child) =>
deferred
.then(() => cloneNode(child, options))
.then((clonedChild: HTMLElement | null) => {
if (clonedChild) {
clonedNode.appendChild(clonedChild);
}
}),
Promise.resolve(),
)
.then(() => clonedNode);
}

function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
const source = window.getComputedStyle(nativeNode);
const target = clonedNode.style;
await children.reduce((deferred, child) => {
const computedStyles = getComputedStyle(nativeNode);
return deferred
.then(() => cloneNode(child, options, false, computedStyles))
.then((clonedChild: HTMLElement | null) => {
if (clonedChild) {
clonedNode.appendChild(clonedChild);
}
});
}, Promise.resolve());

if (!target) {
return;
}
return clonedNode;
}

// eslint-disable-next-line spaced-comment
if (source.cssText) {
target.cssText = source.cssText;
function cloneCSSStyle<T extends HTMLElement>(
nativeNode: T,
clonedNode: T,
parentComputedStyles: CSSStyleDeclaration | null,
) {
const nativeComputedStyle = getComputedStyle(nativeNode);
const sourceStyle = clonedNode.style;
if (nativeComputedStyle.cssText) {
clonedNode.style.cssText = nativeComputedStyle.cssText;
copyFont(nativeComputedStyle, clonedNode.style); // here we re-assign the font props.
} else {
toArray<string>(source).forEach(name => {
target.setProperty(
name,
source.getPropertyValue(name),
source.getPropertyPriority(name),
);
});
copyUserComputedStyleFast(
nativeComputedStyle,
parentComputedStyles,
clonedNode,
);
}

const webkitBackgroundClip = source.getPropertyValue(
const webkitBackgroundClip = sourceStyle.getPropertyValue(
'-webkit-background-clip',
);
if (webkitBackgroundClip !== 'border-box') {
Expand All @@ -104,7 +101,7 @@ function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
}

// fix for flex align bug in safari
const alignItems = source.getPropertyValue('align-items');
const alignItems = sourceStyle.getPropertyValue('align-items');
if (alignItems !== 'normal') {
clonedNode.setAttribute(
'style',
Expand All @@ -113,15 +110,15 @@ function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
}

// fix for perspective bug in safari
const perspective = source.getPropertyValue('perspective');
const perspective = sourceStyle.getPropertyValue('perspective');
if (perspective !== 'none') {
clonedNode.setAttribute(
'style',
`${clonedNode.getAttribute('style')};perspective:${perspective};`,
);
}

const boxShadow = source.getPropertyValue('boxShadow');
const boxShadow = sourceStyle.getPropertyValue('boxShadow');
if (boxShadow !== 'none' && isIOS) {
clonedNode.setAttribute(
'style',
Expand Down Expand Up @@ -156,23 +153,72 @@ function cloneSelectValue<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
async function decorate<T extends HTMLElement>(
nativeNode: T,
clonedNode: T,
parentComputedStyles: CSSStyleDeclaration | null,
): Promise<T> {
if (!(clonedNode instanceof Element)) {
return clonedNode;
}

cloneCSSStyle(nativeNode, clonedNode);
cloneCSSStyle(nativeNode, clonedNode, parentComputedStyles);
clonePseudoElements(nativeNode, clonedNode);
cloneInputValue(nativeNode, clonedNode);
cloneSelectValue(nativeNode, clonedNode);

return clonedNode;
}

async function ensureSVGSymbols<T extends HTMLElement>(
clone: T,
options: Options,
) {
const uses = clone.querySelectorAll ? clone.querySelectorAll('use') : [];
if (uses.length === 0) {
return clone;
}

const processedDefs: {[key: string]: HTMLElement} = {};
for (let i = 0; i < uses.length; i++) {
const use = uses[i];
const id = use.getAttribute('xlink:href');
if (id) {
const exist = clone.querySelector(id);
const definition = document.querySelector(id) as HTMLElement;
if (!exist && definition && !processedDefs[id]) {
// eslint-disable-next-line no-await-in-loop
processedDefs[id] = (await cloneNode(definition, options, true, null))!;
}
}
}

const nodes = Object.values(processedDefs);
if (nodes.length) {
const ns = 'http://www.w3.org/1999/xhtml';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('xmlns', ns);
svg.style.position = 'absolute';
svg.style.width = '0';
svg.style.height = '0';
svg.style.overflow = 'hidden';
svg.style.display = 'none';

const defs = document.createElementNS(ns, 'defs');
svg.appendChild(defs);

for (let i = 0; i < nodes.length; i++) {
defs.appendChild(nodes[i]);
}

clone.appendChild(svg);
}

return clone;
}

export async function cloneNode<T extends HTMLElement>(
node: T,
options: Options,
isRoot?: boolean,
parentComputedStyles?: CSSStyleDeclaration | null,
): Promise<T | null> {
if (!isRoot && options.filter && !options.filter(node)) {
return null;
Expand All @@ -181,5 +227,8 @@ export async function cloneNode<T extends HTMLElement>(
return Promise.resolve(node)
.then(clonedNode => cloneSingleNode(clonedNode, options) as Promise<T>)
.then(clonedNode => cloneChildren(node, clonedNode, options))
.then(clonedNode => decorate(node, clonedNode));
.then(clonedNode =>
decorate(node, clonedNode, parentComputedStyles ?? null),
)
.then(clonedNode => ensureSVGSymbols(clonedNode, options));
}
3 changes: 1 addition & 2 deletions packages/dom-export/src/lib/clonePseudoElements.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {uuid, toArray} from './util';
import {toArray, uuid} from './util';

type Pseudo = ':before' | ':after';

Expand Down Expand Up @@ -43,7 +43,6 @@ function clonePseudoElement<T extends HTMLElement>(
}

const className = uuid();

try {
clonedNode.className = `${clonedNode.className} ${className}`;
} catch (err) {
Expand Down
Loading

0 comments on commit 25c4fdf

Please sign in to comment.