Last active
February 14, 2025 03:04
-
-
Save MrZhouZh/a81fab3f0fbdd58cacec61835bad51a4 to your computer and use it in GitHub Desktop.
Export PDF by html2canvas + jspdf
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import html2canvas from 'html2canvas'; | |
import { message } from 'antd'; | |
export interface ExportLongImgOptions { | |
filename?: string; | |
scale?: number; | |
quality?: number; | |
maxChunkHeight?: number; | |
type?: 'png' | 'jpeg'; | |
backgroundColor?: string | null; | |
compressWidth?: number; | |
enableCompression?: boolean; | |
} | |
const DEFAULT_OPTIONS: Required<ExportLongImgOptions> = { | |
filename: '未命名', | |
scale: 2, | |
quality: 0.6, | |
maxChunkHeight: 3000, | |
type: 'png', | |
backgroundColor: '#ffffff', | |
compressWidth: 1600, | |
enableCompression: false, | |
}; | |
// eslint-disable-next-line no-promise-executor-return | |
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | |
function compressImage( | |
canvas: HTMLCanvasElement, | |
maxWidth: number, | |
quality: number, | |
type: 'png' | 'jpeg', | |
): string { | |
const { width, height } = canvas; | |
let targetWidth = width; | |
let targetHeight = height; | |
const ratio = Math.min(1, maxWidth / width); | |
if (ratio < 1) { | |
targetWidth = width * ratio; | |
targetHeight = height * ratio; | |
} | |
const tempCanvas = document.createElement('canvas'); | |
const ctx = tempCanvas.getContext('2d', { | |
alpha: false, | |
}); | |
if (!ctx) { | |
throw new Error('Cannot get canvas context'); | |
} | |
const intermediateCanvas = document.createElement('canvas'); | |
const intermediateCtx = intermediateCanvas.getContext('2d', { alpha: false }); | |
if (!intermediateCtx) { | |
throw new Error('Cannot get intermediate canvas context'); | |
} | |
const intermediateWidth = targetWidth * 1.5; | |
const intermediateHeight = targetHeight * 1.5; | |
intermediateCanvas.width = intermediateWidth; | |
intermediateCanvas.height = intermediateHeight; | |
intermediateCtx.fillStyle = '#ffffff'; | |
intermediateCtx.fillRect(0, 0, intermediateWidth, intermediateHeight); | |
intermediateCtx.imageSmoothingEnabled = true; | |
intermediateCtx.imageSmoothingQuality = 'high'; | |
intermediateCtx.drawImage( | |
canvas, | |
0, | |
0, | |
intermediateWidth, | |
intermediateHeight, | |
); | |
tempCanvas.width = targetWidth; | |
tempCanvas.height = targetHeight; | |
ctx.fillStyle = '#ffffff'; | |
ctx.fillRect(0, 0, targetWidth, targetHeight); | |
ctx.imageSmoothingEnabled = true; | |
ctx.imageSmoothingQuality = 'high'; | |
ctx.drawImage(intermediateCanvas, 0, 0, targetWidth, targetHeight); | |
intermediateCanvas.remove(); | |
try { | |
const imageData = tempCanvas.toDataURL( | |
`image/${type}`, | |
Math.min(quality, 0.7), | |
); | |
return imageData; | |
} finally { | |
tempCanvas.remove(); | |
} | |
} | |
async function renderChunk( | |
element: HTMLElement, | |
scrollTop: number, | |
chunkHeight: number, | |
options: Required<ExportLongImgOptions>, | |
): Promise<HTMLCanvasElement> { | |
const { scale } = options; | |
const width = element.offsetWidth; | |
const wrapper = document.createElement('div'); | |
wrapper.style.cssText = ` | |
position: fixed; | |
left: -9999px; | |
top: 0; | |
width: ${width}px; | |
height: ${chunkHeight}px; | |
overflow: hidden; | |
background-color: ${options.backgroundColor}; | |
`; | |
const tempContainer = element.cloneNode(true) as HTMLElement; | |
tempContainer.style.cssText = ` | |
position: absolute; | |
top: ${-scrollTop}px; | |
left: 0; | |
width: 100%; | |
transform: none; | |
`; | |
wrapper.appendChild(tempContainer); | |
document.body.appendChild(wrapper); | |
try { | |
await delay(50); | |
return await html2canvas(wrapper, { | |
allowTaint: true, | |
height: chunkHeight, | |
width, | |
scale, | |
useCORS: true, | |
logging: false, | |
backgroundColor: options.backgroundColor, | |
imageTimeout: 15000, | |
onclone: (clonedDoc) => { | |
const clonedWrapper = clonedDoc.querySelector( | |
wrapper.tagName, | |
) as HTMLElement; | |
if (clonedWrapper) { | |
Array.from(clonedWrapper.getElementsByTagName('*')).forEach( | |
(el: Element) => { | |
if (el instanceof HTMLElement) { | |
el.style.transform = 'none'; | |
if (el.style.position === 'fixed') { | |
el.style.position = 'absolute'; | |
} | |
el.style.transition = 'none'; | |
el.style.animation = 'none'; | |
el.style.filter = 'none'; | |
} | |
}, | |
); | |
} | |
}, | |
}); | |
} finally { | |
wrapper.remove(); | |
} | |
} | |
/** | |
* 将 HTML 元素导出为长图片 | |
* | |
* @description | |
* 这个方法可以将任意 HTML 元素导出为长图片,特别适合导出长页面内容。 | |
* 支持分块渲染以处理超长内容,并提供图片压缩功能以优化输出大小。 | |
* | |
* @example | |
* ```typescript | |
* // 基础用法 | |
* const element = document.getElementById('content'); | |
* const imageUrl = await exportLongImg(element); | |
* | |
* // 使用自定义选项 | |
* const imageUrl = await exportLongImg(element, { | |
* filename: '长图导出', | |
* scale: 2, | |
* quality: 0.8, | |
* type: 'png', | |
* maxChunkHeight: 5000, | |
* backgroundColor: '#ffffff', | |
* enableCompression: true, | |
* compressWidth: 1600 | |
* }); | |
* ``` | |
* | |
* @param element - 要导出的 HTML 元素。如果为 null,将抛出错误 | |
* @param options - 导出配置选项 | |
* @param options.filename - 导出文件名,默认为`未命名` | |
* @param options.scale - 导出时的缩放比例,默认为 2,提高这个值可以获得更清晰的效果 | |
* @param options.quality - 图片质量,范围 0-1,默认为 0.6 | |
* @param options.maxChunkHeight - 单次渲染的最大高度,默认为 3000px | |
* @param options.type - 导出图片格式,可选 'png' | 'jpeg',默认为 'png' | |
* @param options.backgroundColor - 背景颜色,默认为 '#ffffff' | |
* @param options.compressWidth - 压缩后的最大宽度,默认为 1600px | |
* @param options.enableCompression - 是否启用压缩,默认为 true | |
* | |
* @returns Promise<string> 返回生成图片的 base64 URL | |
* | |
* @throws | |
* - 如果 element 为 null,将抛出错误 | |
* - 如果无法获取 canvas context,将抛出错误 | |
* - 如果导出过程中出现错误,将显示错误提示 | |
* | |
* @notes | |
* 1. 对于超长内容,会自动进行分块渲染 | |
* 2. 支持 ECharts 等动态内容的导出 | |
* 3. 提供图片压缩功能,可以有效减小输出文件大小 | |
* 4. 建议在导出大型内容时显示 loading 提示 | |
* 5. 如果内容包含跨域图片,需要确保图片支持跨域访问 | |
*/ | |
export async function exportLongImg( | |
element: HTMLElement | null, | |
options: ExportLongImgOptions = {}, | |
): Promise<string> { | |
if (!element) { | |
throw new Error('Element is required'); | |
} | |
const mergedOptions = { | |
...DEFAULT_OPTIONS, | |
...options, | |
} as Required<ExportLongImgOptions>; | |
const { | |
scale, | |
maxChunkHeight, | |
quality, | |
type, | |
enableCompression, | |
compressWidth, | |
} = mergedOptions; | |
const totalHeight = element.scrollHeight; | |
const width = element.offsetWidth; | |
if (totalHeight <= maxChunkHeight) { | |
try { | |
const canvas = await html2canvas(element, { | |
allowTaint: true, | |
height: totalHeight, | |
width, | |
scale, | |
useCORS: true, | |
backgroundColor: mergedOptions.backgroundColor, | |
imageTimeout: 15000, | |
}); | |
if (enableCompression) { | |
return compressImage(canvas, compressWidth, quality, type); | |
} | |
return canvas.toDataURL(`image/${type}`, quality); | |
} catch (error) { | |
console.error('Export image error:', error); | |
message.error('导出失败,请重新点击'); | |
return ''; | |
} | |
} | |
const finalCanvas = document.createElement('canvas'); | |
finalCanvas.width = width * scale; | |
finalCanvas.height = totalHeight * scale; | |
const finalCtx = finalCanvas.getContext('2d'); | |
if (!finalCtx) { | |
throw new Error('Cannot get canvas context'); | |
} | |
if (mergedOptions.backgroundColor) { | |
finalCtx.fillStyle = mergedOptions.backgroundColor; | |
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height); | |
} | |
const chunks = Math.ceil(totalHeight / maxChunkHeight); | |
const canvases: HTMLCanvasElement[] = []; | |
try { | |
const batchSize = 2; | |
const batchCount = Math.ceil(chunks / batchSize); | |
const batchIndexes = Array.from({ length: batchCount }, (_, i) => i); | |
await batchIndexes.reduce(async (promise, batchIndex) => { | |
await promise; | |
const startIndex = batchIndex * batchSize; | |
const currentBatchSize = Math.min(batchSize, chunks - startIndex); | |
const renderPromises = Array.from({ length: currentBatchSize }).map( | |
(_, index) => { | |
const chunkIndex = startIndex + index; | |
const scrollTop = chunkIndex * maxChunkHeight; | |
const remainingHeight = totalHeight - scrollTop; | |
const currentChunkHeight = Math.min(maxChunkHeight, remainingHeight); | |
return renderChunk( | |
element, | |
scrollTop, | |
currentChunkHeight, | |
mergedOptions, | |
); | |
}, | |
); | |
const renderedBatch = await Promise.all(renderPromises); | |
canvases.push(...renderedBatch); | |
renderedBatch.forEach((canvas, index) => { | |
const scrollTop = (startIndex + index) * maxChunkHeight; | |
finalCtx.drawImage( | |
canvas, | |
0, | |
scrollTop * scale, | |
canvas.width, | |
canvas.height, | |
); | |
}); | |
renderedBatch.forEach((canvas) => canvas.remove()); | |
await delay(50); | |
}, Promise.resolve()); | |
if (enableCompression) { | |
return compressImage(finalCanvas, compressWidth, quality, type); | |
} | |
return finalCanvas.toDataURL(`image/${type}`, quality); | |
} catch (error) { | |
console.error('Export image error:', error); | |
message.error('导出失败,请重新点击'); | |
return ''; | |
} finally { | |
canvases.forEach((canvas) => canvas.remove()); | |
finalCanvas.remove(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import html2canvas from 'html2canvas'; | |
import { jsPDF } from 'jspdf'; | |
export interface Options { | |
filename?: string; | |
compress?: boolean; | |
scale?: number; | |
callback?: () => void; | |
} | |
const noop = () => {}; | |
export async function exportPdf( | |
element: HTMLElement | null, | |
options: Options = {}, | |
): Promise<void> { | |
const { | |
filename = '未命名', | |
compress = true, | |
scale = 2, | |
callback = noop, | |
} = options; | |
if (!element) { | |
callback(); | |
return; | |
} | |
const originWidth = element.offsetWidth || 700; | |
const container = document.createElement('div'); | |
container.style.cssText = `position:fixed;left:${-2 * originWidth}px;top:0;padding:0px;width:${originWidth}px;box-sizing:content-box;`; | |
document.body.appendChild(container); | |
container.appendChild(element.cloneNode(true)); | |
const render = async () => { | |
try { | |
const canvas = await html2canvas(container, { scale }); | |
const contentWidth = canvas.width; | |
const contentHeight = canvas.height; | |
// 一页全部展示 | |
// eslint-disable-next-line new-cap | |
const doc = new jsPDF({ | |
orientation: contentWidth > contentHeight ? 'l' : 'p', | |
unit: 'px', | |
format: [contentWidth, contentHeight], | |
compress, | |
}); | |
doc.addImage({ | |
imageData: canvas, | |
format: 'PNG', | |
x: 0, | |
y: 0, | |
width: contentWidth, | |
height: contentHeight, | |
compression: 'FAST', | |
}); | |
doc.save(`${filename}.pdf`); | |
} catch (error) { | |
console.error(`Error rendering PDF:`, error); | |
} finally { | |
container.remove(); | |
callback(); | |
} | |
}; | |
const eleImgs = Array.from(container.querySelectorAll('img')); | |
const { length } = eleImgs; | |
let start = 0; | |
if (length === 0) { | |
await render(); | |
return; | |
} | |
eleImgs.forEach((ele) => { | |
const { src } = ele; | |
if (!src) return; | |
ele.onload = () => { | |
if (!/^http/.test(ele.src)) { | |
start++; | |
if (start === length) { | |
render(); | |
} | |
} | |
}; | |
fetch(src) | |
.then((res) => res.blob()) | |
.then((blob) => { | |
const reader = new FileReader(); | |
reader.onload = () => { | |
ele.src = reader.result as string; | |
}; | |
reader.readAsDataURL(blob); | |
return true; | |
}) | |
.catch((error) => { | |
start++; | |
console.error(`Error fetching image:`, error); | |
if (start === length) { | |
render(); | |
} | |
}); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import html2canvas from 'html2canvas'; | |
import { jsPDF } from 'jspdf'; | |
import { message } from 'antd'; | |
export interface Options { | |
filename?: string; | |
compress?: boolean; | |
compression?: 'NONE' | 'FAST' | 'MEDIUM' | 'SLOW'; | |
scale?: number; | |
quality?: number; | |
callback?: () => void; | |
} | |
const noop = () => {}; | |
const DEFAULT_OPTIONS: Required<Options> = { | |
filename: '未命名', | |
compress: true, | |
compression: 'FAST', | |
scale: 2, | |
quality: 0.8, | |
callback: noop, | |
}; | |
// A4 纸的尺寸(像素,以 96 DPI 为基准) | |
const A4_WIDTH_PT = 595; | |
const A4_HEIGHT_PT = 842; | |
// 页面边距(点) | |
const MARGIN_PT = 0; | |
// 添加延迟函数 | |
// eslint-disable-next-line no-promise-executor-return | |
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | |
async function renderChunk( | |
element: HTMLElement, | |
scrollTop: number, | |
chunkHeight: number, | |
options: Required<Options>, | |
): Promise<HTMLCanvasElement> { | |
const { scale } = options; | |
const width = element.offsetWidth; | |
// 创建临时容器 | |
const wrapper = document.createElement('div'); | |
wrapper.style.cssText = ` | |
position: fixed; | |
left: -9999px; | |
top: 0; | |
width: ${width}px; | |
height: ${chunkHeight}px; | |
overflow: hidden; | |
background-color: ${getComputedStyle(element).backgroundColor}; | |
`; | |
// 克隆元素并设置样式 | |
const tempContainer = element.cloneNode(true) as HTMLElement; | |
tempContainer.style.cssText = ` | |
position: absolute; | |
top: ${-scrollTop}px; | |
left: 0; | |
width: 100%; | |
transform: none; | |
`; | |
wrapper.appendChild(tempContainer); | |
document.body.appendChild(wrapper); | |
try { | |
// 添加小延迟确保DOM渲染完成 | |
await delay(50); | |
const canvas = await html2canvas(wrapper, { | |
allowTaint: true, | |
height: chunkHeight, | |
width, | |
scale, | |
useCORS: true, | |
logging: false, | |
backgroundColor: null, | |
onclone: (clonedDoc) => { | |
const clonedWrapper = clonedDoc.querySelector( | |
wrapper.tagName, | |
) as HTMLElement; | |
if (clonedWrapper) { | |
Array.from(clonedWrapper.getElementsByTagName('*')).forEach( | |
(el: Element) => { | |
if (el instanceof HTMLElement) { | |
el.style.transform = 'none'; | |
if (el.style.position === 'fixed') { | |
el.style.position = 'absolute'; | |
const originalTop = parseInt(el.style.top || '0', 10); | |
el.style.top = `${originalTop - scrollTop}px`; | |
} | |
} | |
}, | |
); | |
} | |
}, | |
}); | |
return canvas; | |
} finally { | |
wrapper.remove(); | |
} | |
} | |
/** | |
* 将 HTML 元素导出为 PDF 文件 | |
* | |
* @description | |
* 这个方法可以将任意 HTML 元素导出为 PDF 文件,支持长页面分页处理, | |
* 并且会自动调整内容以适应 A4 纸张大小。 | |
* | |
* @example | |
* ```typescript | |
* // 基础用法 | |
* const element = document.getElementById('content'); | |
* await exportPdf(element); | |
* | |
* // 使用自定义选项 | |
* await exportPdf(element, { | |
* filename: '我的文档', | |
* scale: 2, | |
* quality: 0.9, | |
* callback: () => console.log('导出完成') | |
* }); | |
* ``` | |
* | |
* @param element - 要导出的 HTML 元素。如果为 null,将直接返回并显示错误提示 | |
* @param options - 导出配置选项 | |
* @param options.filename - PDF 文件名,默认为 "未命名" | |
* @param options.compress - 是否压缩 PDF,默认为 true | |
* @param options.compression - 压缩级别,可选 'NONE' | 'FAST' | 'MEDIUM' | 'SLOW',默认为 'FAST' | |
* @param options.scale - 导出时的缩放比例,默认为 2,提高这个值可以获得更清晰的效果,但会增加文件大小 | |
* @param options.quality - 图片质量,范围 0-1,默认为 0.8 | |
* @param options.callback - 导出完成后的回调函数 | |
* | |
* @returns Promise<void> | |
* | |
* @throws | |
* - 如果 element 为 null,将显示错误提示 | |
* - 如果导出过程中出现错误,将显示错误提示 | |
* | |
* @notes | |
* 1. 该方法会自动处理长页面的分页 | |
* 2. 支持 ECharts 等动态内容的导出 | |
* 3. 自动适配 A4 纸张大小 | |
* 4. 建议在较大内容上使用时增加 loading 提示 | |
*/ | |
export async function exportPdf( | |
element: HTMLElement | null, | |
options: Options = {}, | |
): Promise<void> { | |
if (!element) { | |
options.callback?.(); | |
message.error('导出失败,请重新点击'); | |
return; | |
} | |
const mergedOptions = { ...DEFAULT_OPTIONS, ...options }; | |
const { filename, compress, compression, scale, callback } = mergedOptions; | |
try { | |
const totalHeight = element.scrollHeight; | |
const width = element.offsetWidth; | |
// 计算实际可用区域(考虑边距) | |
const contentWidth = A4_WIDTH_PT - 2 * MARGIN_PT; | |
const contentHeight = A4_HEIGHT_PT - 2 * MARGIN_PT; | |
// 计算缩放比例 | |
const widthScale = contentWidth / width; | |
const scaledTotalHeight = totalHeight * widthScale; | |
// 计算每个分块的高度和实际页数 | |
const contentChunkHeight = Math.floor(contentHeight / widthScale); | |
// const contentChunkHeight = Math.ceil(contentHeight / widthScale); | |
const fullPages = Math.floor(totalHeight / contentChunkHeight); | |
const remainingHeight = totalHeight % contentChunkHeight; | |
// 总页数:完整页数 + (如果有剩余内容则加1) | |
const chunks = remainingHeight > 0 ? fullPages + 1 : fullPages; | |
// 创建 PDF 文档 | |
// eslint-disable-next-line new-cap | |
const doc = new jsPDF({ | |
orientation: 'p', | |
unit: 'pt', | |
format: 'a4', | |
compress, | |
}); | |
// 创建批次索引数组 | |
const batchSize = 2; // 每批处理的块数 | |
const batchCount = Math.ceil(chunks / batchSize); | |
const batchIndexes = Array.from({ length: batchCount }, (_, i) => i); | |
// 使用 reduce 串行处理批次 | |
await batchIndexes.reduce(async (promise, batchIndex) => { | |
await promise; | |
const startIndex = batchIndex * batchSize; | |
const currentBatchSize = Math.min(batchSize, chunks - startIndex); | |
const renderPromises = Array.from({ length: currentBatchSize }).map( | |
(_, index) => { | |
const chunkIndex = startIndex + index; | |
const scrollTop = chunkIndex * contentChunkHeight; | |
// 计算当前块的实际高度 | |
let currentChunkHeight; | |
if (chunkIndex === fullPages && remainingHeight > 0) { | |
// 最后一页,使用剩余高度 | |
currentChunkHeight = remainingHeight; | |
} else { | |
// 完整页面 | |
currentChunkHeight = contentChunkHeight; | |
} | |
return renderChunk( | |
element, | |
scrollTop, | |
currentChunkHeight, | |
mergedOptions, | |
); | |
}, | |
); | |
try { | |
const renderedBatch = await Promise.all(renderPromises); | |
renderedBatch.forEach((canvas, index) => { | |
const pageIndex = startIndex + index; | |
const scrollTop = pageIndex * contentChunkHeight; | |
// 计算当前页的实际高度 | |
let currentPageHeight; | |
if (pageIndex === fullPages && remainingHeight > 0) { | |
// 最后一页,使用实际剩余高度 | |
currentPageHeight = | |
(remainingHeight / contentChunkHeight) * A4_HEIGHT_PT; | |
} else { | |
// 完整页面 | |
currentPageHeight = A4_HEIGHT_PT; | |
} | |
if (pageIndex > 0) { | |
doc.addPage(); | |
} | |
// 添加图片到 PDF,考虑边距 | |
doc.addImage({ | |
imageData: canvas.toDataURL('image/jpeg', mergedOptions.quality), | |
format: 'JPEG', | |
x: MARGIN_PT, | |
y: MARGIN_PT, | |
width: contentWidth, | |
height: currentPageHeight - 2 * MARGIN_PT, | |
compression, | |
}); | |
canvas.remove(); | |
if (canvas.parentNode) { | |
canvas.parentNode.removeChild(canvas); | |
} | |
}); | |
if (window.gc) { | |
window.gc(); | |
} | |
} catch (err) { | |
console.error(`Error processing batch ${batchIndex}:`, err); | |
throw err; | |
} | |
await delay(100); | |
}, Promise.resolve()); | |
doc.save(`${filename}.pdf`); | |
} catch (error) { | |
console.error(`Error rendering PDF:`, error); | |
message.error('导出失败,请重新点击'); | |
} finally { | |
callback(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* refs: https://juejin.cn/post/7412672713376497727 | |
*/ | |
import html2canvas from 'html2canvas'; | |
import jsPDF from 'jspdf'; | |
export class DomToPdf { | |
private _rootDom: HTMLElement | null; | |
private _title: string; | |
private _a4Width: number; | |
private _a4Height: number; | |
private _pageBackground: string; | |
private _hex: [number, number, number]; | |
constructor(rootDom: HTMLElement | null, title: string, color: [number, number, number] = [255, 255, 255]) { | |
this._rootDom = rootDom; | |
this._title = title; | |
this._a4Width = 595.266; | |
this._a4Height = 841.89; | |
this._pageBackground = `rgb(${color[0]},${color[1]},${color[2]})`; | |
this._hex = color; | |
} | |
async savePdf(): Promise<jsPDF> { | |
const a4Width = this._a4Width; | |
const a4Height = this._a4Height; | |
const hex = this._hex; | |
return new Promise<jsPDF>(async (resolve, reject) => { | |
try { | |
if (!this._rootDom) { | |
throw new Error("Root DOM element is null."); | |
} | |
const canvas = await html2canvas(this._rootDom, { | |
useCORS: true, | |
allowTaint: true, | |
scale: 0.8, | |
backgroundColor: this._pageBackground, | |
}); | |
const pdf = new jsPDF('p', 'pt', 'a4'); | |
let index = 1; | |
let canvas1 = document.createElement('canvas'); | |
let height: number; | |
let leftHeight = canvas.height; | |
const a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height); | |
let position = 0; | |
let pageData = canvas.toDataURL('image/jpeg', 0.7); | |
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen'); | |
const createImpl = (canvas: HTMLCanvasElement) => { | |
if (leftHeight > 0) { | |
index++; | |
let checkCount = 0; | |
if (leftHeight > a4HeightRef) { | |
let i = position + a4HeightRef; | |
for (i = position + a4HeightRef; i >= position; i--) { | |
let isWrite = true; | |
for (let j = 0; j < canvas.width; j++) { | |
const ctx = canvas.getContext('2d'); | |
if (!ctx) { | |
throw new Error("Could not get 2D context from canvas."); | |
} | |
let c = ctx.getImageData(j, i, 1, 1).data; | |
if (c[0] !== hex[0] || c[1] !== hex[1] || c[2] !== hex[2]) { | |
isWrite = false; | |
break; | |
} | |
} | |
if (isWrite) { | |
checkCount++; | |
if (checkCount >= 10) { | |
break; | |
} | |
} else { | |
checkCount = 0; | |
} | |
} | |
height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef); | |
if (height <= 0) { | |
height = a4HeightRef; | |
} | |
} else { | |
height = leftHeight; | |
} | |
canvas1.width = canvas.width; | |
canvas1.height = height; | |
const ctx = canvas1.getContext('2d'); | |
if (!ctx) { | |
throw new Error("Could not get 2D context from canvas1."); | |
} | |
ctx.drawImage( | |
canvas, | |
0, | |
position, | |
canvas.width, | |
height, | |
0, | |
0, | |
canvas.width, | |
height, | |
); | |
if (position !== 0) { | |
pdf.addPage(); | |
} | |
pdf.setFillColor(hex[0], hex[1], hex[2]); | |
pdf.rect(0, 0, a4Width, a4Height, 'F'); | |
pdf.addImage( | |
canvas1.toDataURL('image/jpeg', 1.0), | |
'JPEG', | |
0, | |
0, | |
a4Width, | |
(a4Width / canvas1.width) * height, | |
); | |
leftHeight -= height; | |
position += height; | |
if (leftHeight > 0) { | |
setTimeout(createImpl, 500, canvas); | |
} else { | |
resolve(pdf); | |
} | |
} | |
}; | |
if (leftHeight < a4HeightRef) { | |
pdf.setFillColor(hex[0], hex[1], hex[2]); | |
pdf.rect(0, 0, a4Width, a4Height, 'F'); | |
pdf.addImage( | |
pageData, | |
'JPEG', | |
0, | |
0, | |
a4Width, | |
(a4Width / canvas.width) * leftHeight, | |
); | |
resolve(pdf); | |
} else { | |
try { | |
pdf.deletePage(0); | |
setTimeout(createImpl, 500, canvas); | |
} catch (err) { | |
reject(err); | |
} | |
} | |
} catch (error) { | |
reject(error); | |
} | |
}); | |
} | |
async downToPdf(setLoadParent: (loading: boolean) => void): Promise<void> { | |
setLoadParent(true); | |
const newPdf = await this.savePdf(); | |
const title = this._title; | |
newPdf.save(title + '.pdf'); | |
setLoadParent(false); | |
} | |
async printToPdf(setLoadParent: (loading: boolean) => void): Promise<void> { | |
setLoadParent(true); | |
const newPdf = await this.savePdf(); | |
const pdfBlob = newPdf.output('blob'); | |
const pdfUrl = URL.createObjectURL(pdfBlob); | |
setLoadParent(false); | |
window.open(pdfUrl); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment