Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add usePageCss option, add toCanvasList and toImage method for large html #473

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ All the top level functions accept DOM node and rendering options, and return a
- [toSvg](#toSvg)
- [toJpeg](#toJpeg)
- [toBlob](#toBlob)
- [toImage](#toImage)
- [toCanvas](#toCanvas)
- [toCanvasList](#toCanvasList)
- [toPixelData](#toPixelData)

Go with the following examples.
Expand Down Expand Up @@ -114,6 +116,16 @@ htmlToImage.toBlob(document.getElementById('my-node'))
});
```

#### toImage
Get a HTMLImageElement, which is a svg image that you can scale it to a big size and it will not blurred.

```js
htmlToImage.toImage(document.getElementById('my-node'))
.then(function (img) {
document.body.appendChild(img);
});
```

#### toCanvas
Get a HTMLCanvasElement, and display it right away:

Expand All @@ -124,6 +136,17 @@ htmlToImage.toCanvas(document.getElementById('my-node'))
});
```

#### toCanvasList
Get a array of HTMLCanvasElement. Not like `toCanvas` which is limited by [canvas size](https://jhildenbiddle.github.io/canvas-size/#/?id=test-results),
`toCanvasList` can get rid of the limitation of canvas size, so this can export a very large html:

```js
htmlToImage.toCanvasList(document.getElementById('my-node'))
.then(function (canvasList) {
canvasList.map(canvas => document.body.appendChild(canvas));
});
```

#### toPixelData
Get the raw pixel data as a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) with every 4 array elements representing the RGBA data of a pixel:

Expand Down Expand Up @@ -292,6 +315,15 @@ A string indicating the image format. The default type is image/png; that type i
When supplied, the toCanvas function will return a blob matching the given image type and quality.

Defaults to `image/png`


### usePageCss

Use `true` to add a `<style>` tag in svg content which imports all styles of current html page, and do not add computed styles to every node any more(this make svg content so large that Firefox throw errors while load svg as image).
This will make the svg content much smaller, to resolve problems which caused by html that has large amount of sub nodes.


Defaults to `false`

## Browsers

Expand Down
12 changes: 6 additions & 6 deletions src/clone-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
) {
value = 'block'
}

if (name === 'd' && clonedNode.getAttribute('d')) {
value = `path(${clonedNode.getAttribute('d')})`
}

targetStyle.setProperty(
name,
value,
Expand Down Expand Up @@ -170,10 +170,10 @@ function cloneSelectValue<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
}
}

function decorate<T extends HTMLElement>(nativeNode: T, clonedNode: T): T {
function decorate<T extends HTMLElement>(nativeNode: T, clonedNode: T, usePageCss?: boolean): T {
if (isInstanceOfElement(clonedNode, Element)) {
cloneCSSStyle(nativeNode, clonedNode)
clonePseudoElements(nativeNode, clonedNode)
if (!usePageCss) cloneCSSStyle(nativeNode, clonedNode)
if (!usePageCss) clonePseudoElements(nativeNode, clonedNode)
cloneInputValue(nativeNode, clonedNode)
cloneSelectValue(nativeNode, clonedNode)
}
Expand Down Expand Up @@ -240,6 +240,6 @@ 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, options.usePageCss))
.then((clonedNode) => ensureSVGSymbols(clonedNode, options))
}
58 changes: 54 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
canvasToBlob,
nodeToDataURL,
checkCanvasDimensions,
getDimensionLimit,
} from './util'

export async function toSvg<T extends HTMLElement>(
Expand All @@ -21,18 +22,24 @@
await embedWebFonts(clonedNode, options)
await embedImages(clonedNode, options)
applyStyle(clonedNode, options)
const datauri = await nodeToDataURL(clonedNode, width, height)
const datauri = await nodeToDataURL(clonedNode, width, height, options.usePageCss)
return datauri
}

export async function toImage<T extends HTMLElement>(
node: T,
options: Options = {},
): Promise<HTMLImageElement> {
const svg = await toSvg(node, options)
return createImage(svg)
}

export async function toCanvas<T extends HTMLElement>(
node: T,
options: Options = {},
): Promise<HTMLCanvasElement> {
const img = await toImage(node, options)
const { width, height } = getImageSize(node, options)
const svg = await toSvg(node, options)
const img = await createImage(svg)

const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')!
const ratio = options.pixelRatio || getPixelRatio()
Expand All @@ -58,6 +65,49 @@
return canvas
}

export async function toCanvasList<T extends HTMLElement>(
node: T,
options: Options = {},
): Promise<Array<HTMLCanvasElement>> {
const img = await toImage(node, options)
const { width, height } = getImageSize(node, options)
const ratio = options.pixelRatio || getPixelRatio()
let canvasWidth = (options.canvasWidth || width) * ratio
let canvasHeight = (options.canvasHeight || height) * ratio
const dimensionLimit = getDimensionLimit()
if (canvasWidth > dimensionLimit) {
canvasHeight *= dimensionLimit / canvasWidth
canvasWidth = dimensionLimit
}

const result: Array<HTMLCanvasElement> = []
const scale = canvasWidth / img.width
for (let curY = 0; curY < canvasHeight; curY += dimensionLimit) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')!
const height1 = Math.min(canvasHeight - curY, dimensionLimit)
canvas.width = canvasWidth
canvas.height = height1
if (options.backgroundColor) {
context.fillStyle = options.backgroundColor
context.fillRect(0, 0, canvas.width, canvas.height)
}
context.drawImage(
img,
0,
curY / scale,
canvasWidth / scale,
height1 / scale,

Check warning on line 100 in src/index.ts

View check run for this annotation

Codecov / codecov/patch

src/index.ts#L100

Added line #L100 was not covered by tests
0,
0,
canvasWidth,
height1,
)
result.push(canvas)
}
return result
}

export async function toPixelData<T extends HTMLElement>(
node: T,
options: Options = {},
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,9 @@ export interface Options {
*
*/
fetchRequestInit?: RequestInit
/*
* Use a <style> in svg to import all styles of current html page, and do not add computed styles to every node any more.
* This will make the svg content very small, to resolve problems when html has large amount of sub nodes.
* */
usePageCss?: boolean
}
86 changes: 80 additions & 6 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,62 @@
canvas.height *= canvasDimensionLimit / canvas.width
canvas.width = canvasDimensionLimit
} else {
canvas.width *= canvasDimensionLimit / canvas.height
canvas.height = canvasDimensionLimit
const height1 = getMaxCanvasHeight(canvas.width)
canvas.width *= height1 / canvas.height

Check warning on line 133 in src/util.ts

View check run for this annotation

Codecov / codecov/patch

src/util.ts#L132-L133

Added lines #L132 - L133 were not covered by tests
canvas.height = height1
}
} else if (canvas.width > canvasDimensionLimit) {
canvas.height *= canvasDimensionLimit / canvas.width
canvas.width = canvasDimensionLimit
} else {
canvas.width *= canvasDimensionLimit / canvas.height
canvas.height = canvasDimensionLimit
const height = getMaxCanvasHeight(canvas.width)

Check warning on line 140 in src/util.ts

View check run for this annotation

Codecov / codecov/patch

src/util.ts#L140

Added line #L140 was not covered by tests
canvas.width *= height / canvas.height
canvas.height = height
}
}
}

export function getDimensionLimit(): number {
return canvasDimensionLimit
}

const dimenstionLimitCache: { [width: number]: number } = {}

export function getMaxCanvasHeight(width: number): number {
let val = dimenstionLimitCache[width]
if (val) return val
val = test()
dimenstionLimitCache[width] = val
return val

function test(): number {

Check warning on line 160 in src/util.ts

View check run for this annotation

Codecov / codecov/patch

src/util.ts#L159-L160

Added lines #L159 - L160 were not covered by tests
const heights = [
// Chrome 83 (Mac, Win)
65535,
// Chrome 70 (Mac, Win)
// Chrome 68 (Android 4.4-9)
// Firefox 63 (Mac, Win)
32767,
// Edge 17 (Win)
// IE11 (Win)

Check warning on line 169 in src/util.ts

View check run for this annotation

Codecov / codecov/patch

src/util.ts#L168-L169

Added lines #L168 - L169 were not covered by tests
// 16384,
]
for (let i = 0; i < heights.length; i++) {

Check warning on line 172 in src/util.ts

View check run for this annotation

Codecov / codecov/patch

src/util.ts#L171-L172

Added lines #L171 - L172 were not covered by tests
try {
const canvas = document.createElement('canvas')
canvas.width = width

Check warning on line 175 in src/util.ts

View check run for this annotation

Codecov / codecov/patch

src/util.ts#L175

Added line #L175 was not covered by tests
canvas.height = heights[i]
const ctx = canvas.getContext('2d')!
ctx.drawImage(new Image(), 0, 0) // check
return heights[i]
} catch (e) {
// ignore
}
}
return canvasDimensionLimit
}
}

export function canvasToBlob(
canvas: HTMLCanvasElement,
options: Options = {},
Expand Down Expand Up @@ -203,13 +246,16 @@
node: HTMLElement,
width: number,
height: number,
usePageCss?: boolean,
): Promise<string> {
const xmlns = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(xmlns, 'svg')
const foreignObject = document.createElementNS(xmlns, 'foreignObject')

svg.setAttribute('width', `${width}`)
svg.setAttribute('height', `${height}`)
// fix: if ratio=2 and style.border='1px', in html it is actually rendered to 1px, but in <img src="svg"> it is rendered to 2px. Then height is different and the bottom 1px is lost, 10 nodes will lost 10px.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This image is missing a text alternative. This is a problem for people using screen readers.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This image is missing a text alternative. This is a problem for people using screen readers.

const ratio = getPixelRatio()
svg.setAttribute('width', `${width / ratio}`)
svg.setAttribute('height', `${height / ratio}`)
svg.setAttribute('viewBox', `0 0 ${width} ${height}`)

foreignObject.setAttribute('width', '100%')
Expand All @@ -220,6 +266,12 @@

svg.appendChild(foreignObject)
foreignObject.appendChild(node)
if (usePageCss) {
const style = document.createElementNS(xmlns, 'style')
style.innerHTML = await getStyles()
svg.insertBefore(style, foreignObject)
}

return svgToDataURL(svg)
}

Expand All @@ -240,3 +292,25 @@
isInstanceOfElement(nodePrototype, instance)
)
}

export function getStyles() {
const styles = document.querySelectorAll('style,link[rel="stylesheet"]')
const promises: Array<Promise<string>> = []
toArray(styles).forEach((el) => {
const e = el as Element
if (e.tagName === 'LINK') {
const href = e.getAttribute('href')
if (href)
promises.push(
fetch(href)
.then((r) => r.text())
.catch(() => ''),
)
} else {
promises.push(Promise.resolve(e.innerHTML))
}
})
return Promise.all(promises).then((arr) => {
return arr.join('\n\n')
})
}
Loading