Skip to content

Commit

Permalink
feat: add toCanvasList and toImage method to get rid of the limit…
Browse files Browse the repository at this point in the history
…ation of canvas size, so this can export a very large htm.

And use a ``getMaxCanvasHeight` method to raise the canvas size limit from 16384 to 65535(Chrome) or 32767(Firefox)
  • Loading branch information
hzsrc committed Aug 17, 2024
1 parent 5b7cfe2 commit 8ea57e6
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 16 deletions.
23 changes: 23 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
56 changes: 53 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
canvasToBlob,
nodeToDataURL,
checkCanvasDimensions,
getDimensionLimit,
} from './util'

export async function toSvg<T extends HTMLElement>(
Expand All @@ -25,14 +26,20 @@ export async function toSvg<T extends HTMLElement>(
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 @@ export async function toCanvas<T extends HTMLElement>(
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
70 changes: 57 additions & 13 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,62 @@ export function checkCanvasDimensions(canvas: HTMLCanvasElement) {
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 @@ -210,7 +253,7 @@ export async function nodeToDataURL(
const foreignObject = document.createElementNS(xmlns, 'foreignObject')

// 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.
var ratio = getPixelRatio();
const ratio = getPixelRatio()
svg.setAttribute('width', `${width / ratio}`)
svg.setAttribute('height', `${height / ratio}`)
svg.setAttribute('viewBox', `0 0 ${width} ${height}`)
Expand Down Expand Up @@ -252,21 +295,22 @@ export const isInstanceOfElement = <

export function getStyles() {
const styles = document.querySelectorAll('style,link[rel="stylesheet"]')
const ps: Array<Promise<string>> = []
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) ps.push(getCssText(href).catch(() => ''))
if (href)
promises.push(
fetch(href)
.then((r) => r.text())
.catch(() => ''),
)
} else {
ps.push(Promise.resolve(e.innerHTML))
promises.push(Promise.resolve(e.innerHTML))
}
})
return Promise.all(ps).then((arr) => {
return Promise.all(promises).then((arr) => {
return arr.join('\n\n')
})

function getCssText(url: string) {
return fetch(url).then((r) => r.text())
}
}

0 comments on commit 8ea57e6

Please sign in to comment.