1、簡要描述
上一篇博客主要講的是pdf文件轉(zhuǎn)換成canvas,然后進行相關(guān)的畫框截圖操作。
【PDF】Canvas繪制PDF及截圖
本篇博客主要講html中dom如何生成pdf文件(前端生成pdf),后端生成pdf當(dāng)然也可以,原理也是將html網(wǎng)頁通過后端服務(wù)導(dǎo)出成pdf,然后css設(shè)置break-after:always;作為分頁邏輯,但是我們不深入講,這里著重講前端生成pdf。
2、相關(guān)插件及知識
還是使用的老朋友jspdf插件和html2canvas
1、jspdf
"jspdf": "^2.5.1"
?使用方法:
import JsPDF from 'jspdf';
const PDF = new jsPDF({
unit: "mm", // 單位,本示例為mm
format: "a4", // 頁面大小
orientation: "portrait", // 頁面方向,portrait: 縱向,landscape: 橫向
putOnlyUsedFonts: true, // 只包含使用的字體
compress: true, // 壓縮文檔
precision: 16, // 浮點數(shù)的精度
});
// 或者
const PDF = new JsPDF('p', 'mm', [210, 297]);
<!-- 常用方法 -->
// 添加圖片
PDF.addImage(
imageData, // 此值可以為下面這些類型 string | HTMLImageElement | HTMLCanvasElement | Uint8Array | RGBAData
'JPEG', // 轉(zhuǎn)換后的格式
x, // 被切割的imageData的橫坐標(biāo)
y, // 被切割的imageData的縱坐標(biāo)
w, // 當(dāng)前圖片的寬度
h, // 當(dāng)前圖片的高度
);
// 添加新的一頁
PDF.addPage();
// 輸出格式
PDF.output(type: "arraybuffer"): ArrayBuffer;
PDF.output(type: "blob"): Blob;
PDF.output(type: "bloburi" | "bloburl"): URL;
// 本地保存為pdf文件
PDF.save('lindadayo.pdf')
2、html2canvas
"html2canvas": "^1.4.1"
// 實例方法
html2canvas(dom, config).then(function(canvas) {})
?config相關(guān)配置參考下圖:
3、源碼
1、dom結(jié)構(gòu)
2、核心邏輯
這里為什么要將dom進行分區(qū)處理呢?請看第四點疑難解答中1、為什么要對dom進行分區(qū)操作?
/**
* 生成pdf
* @param CommonPage 需要轉(zhuǎn)換的dom節(jié)點
* @param i 分區(qū)索引
* @returns
*/
async generatePdf(CommonPage?: Element, childLen?: number) {
PDF = new JsPDF('p', 'mm', [210, 297]); // pdf實例
for (let i = 0; i < childLen; i++) {
await asyncSingleAreaControl(CommonPage, i)
}
generateUploadPdf();
},
/**
* 上傳pdf文件
*/
async generateUploadPdf() {
// 文件重命名,修改生成pdf后的文件名
const pdfName = pdfNameHandle()
const uri = PDF.output('blob')
const file = await blobUriToFile(uri, pdfName)
// 此時的file是File類對象,你可以選擇上傳到服務(wù)器噢~當(dāng)然你也可以選擇直接導(dǎo)出到前端
// PDF.output('lindadayo.pdf');
},
/**
* 單個分區(qū)生成pdf操作
* @param CommonPage 父節(jié)點dom
* @param i 分區(qū)索引
* @returns
*/
async asyncSingleAreaControl(CommonPage, i) {
const canvas = await singleHandle(CommonPage, i)
await areaPage(canvas, i)
},
/**
* 分區(qū)pdf處理
* @param canvas 各個分區(qū)dom轉(zhuǎn)換后的canvas
* @param areaNo 分區(qū)索引
*/
areaPage(canvas, areaNo) {
// 是否是第一個分區(qū)(作用于是否開始就addPage)
const isFirstArea = areaNo === 0
return new Promise((resolve, _reject) => {
// a4紙寬高
const A4Origin = {
width: PDF.internal.pageSize.getWidth(),
height: PDF.internal.pageSize.getHeight()
}
const contentWidth = canvas.width;
/**
* html2canvas放大3.125倍時精度丟失導(dǎo)致多了2像素
* 3368: 高度285mm紙張html2canvas放大300dpi后像素
* 3366:正常實際高度
*/
const contentHeight = canvas.height <= 3368 ? 3366 : canvas.height;
const pageHeight = Math.round(contentWidth / A4Origin.width * A4Origin.height);
let leftHeight = contentHeight;
let position = 0;
const imgWidth = A4Origin.width;
const imgHeight = Math.ceil(A4Origin.width / contentWidth * contentHeight);
const pageData = canvas.toDataURL('image/jpeg', 1);
// 非首個分區(qū),得先addPage,因為不然會少一頁 && 大于某個范圍才新增一頁,避免因為浮點數(shù)計算精度造成多增一頁
if (!isFirstArea && leftHeight > 0) {
PDF.addPage()
}
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth + (isBrower() ? 0.62 : 0), imgHeight + (isBrower() ? 0.32 : 0));
position -= A4Origin.height;
leftHeight -= pageHeight
// 大于某個范圍才新增一頁,避免因為浮點數(shù)計算精度造成多增一頁
if (leftHeight > 0) {
PDF.addPage()
}
}
resolve(true)
})
},
/**
* 單頁pdf處理
// * @param root 總節(jié)點
* @param index 分區(qū)索引
*/
async singleHandle(CommonPage, index) {
// 報錯Unable to find element in cloned iframe解決方法
// getDiv在外部聲明, 內(nèi)部賦值
try {
getDiv = CommonPage.querySelector(`#CommonPageItemArea-${index}`)
const res = await html2canvas(getDiv, {
useCORS: true,
allowTaint: true,
scale: 3.125
}).then(function(canvas) {
return canvas
})
return res
} catch (e) {
console.log(e)
}
}
4、疑難解答
1、為什么要對dom進行分區(qū)操作?
其實如果你不使用html2canvas的參數(shù)scale,就沒必要進行分區(qū),但是在很多時候,你不放大canvas的話,會導(dǎo)致pdf中的圖片很模糊,還有鋸齒,所以要對canvas進行方法,但是放大后,會導(dǎo)致一些問題:生成pdf后,超過15000px以后的dom會有樣式丟失,所以得對dom進行分區(qū)操作,讓每個分區(qū)的dom高度 * 放大倍數(shù)不超過15000px。我們一般都會導(dǎo)出a4紙大小,a4紙寬高是210mm*297mm,換算成像素是793.29px * 1122.52px,如果你選擇放大兩倍,那么,單頁高度就是2245px,結(jié)論為一個分區(qū)能夠放六個a4紙高度的dom,所以你在開發(fā)頁面時,就要做好這種頁面結(jié)構(gòu)噢~
2、html2canvas仍然報圖片出錯/跨域的問題,即使后端oss已經(jīng)解決跨域了
報錯Error loading image
這個涉及知識點:img標(biāo)簽實例化獲取屬于非跨域操作,Image類實例化屬于跨域操作,所以得再html2canvas依賴中打補丁,當(dāng)圖片是你本地的靜態(tài)圖片,那不需要轉(zhuǎn),還是按照Image實例化來做,當(dāng)圖片已經(jīng)是base64格式的話,也不需要轉(zhuǎn),賦值給img標(biāo)簽,否則的話加上隨機數(shù)。
/dist/html2canvas.js 第5759行
3、報錯Unable to find element in cloned iframe解決方法
在分區(qū)中循環(huán)處理dom生成canvas時會報出這種錯誤,原因是html2canvas第一參數(shù)的變量應(yīng)該設(shè)置為全局變量而不應(yīng)該是局部變量
try {
getDiv = CommonPage.querySelector(`#CommonPageItemArea-${index}`)
const res = await html2canvas(getDiv, {
useCORS: true,
allowTaint: true,
scale: 3.125
}).then(function(canvas) {
return canvas
})
return res
} catch (e) {
console.log(e)
}
4、dom-to-image和html2canvas相比,哪個更優(yōu)?
dom-to-image是一個js庫,可以將任意dom節(jié)點轉(zhuǎn)換為矢量(SVG)或光柵(PNG或JPEG)圖像。和html2canvas相比的話,算是一個新起之秀,更輕巧,相同點就是都會先將dom轉(zhuǎn)成canvas進行操作,所以在dom層級深和多的情況下,還是建議使用html2canvas這種老牌插件
5、生成pdf里圖片缺失
那是因為圖片轉(zhuǎn)換及獲取是異步的,需要時間渲染,所以生成pdf的步驟應(yīng)該在圖片完全加載完之后,由此我們可以加個定時器來循環(huán)判斷全部圖片是否加載完成,加載完成再進行生成操
/**
* CommonPage生成dom渲染完成
* @param callback
*/
commonPageLoadFinish(callback) {
nextTick(() => {
// 生成節(jié)點
const CommonPage = document.querySelector('#CommonPage')
const childLen = CommonPage.querySelectorAll('.CommonPageItemArea').length
console.log('分區(qū)數(shù)量', childLen)
if (childLen > 0) {
let timer = null;
// 監(jiān)聽頁面中所有轉(zhuǎn)base64圖片是否已生成完畢,如果已生成完畢,則進入下一步與dom相關(guān)的操作
timer = setInterval(() => {
// isImageAllCompleted.sum => 圖片總數(shù)量, isImageAllCompleted.loadSum => 目前圖片已經(jīng)加載完的數(shù)量
if (isImageAllCompleted.sum === isImageAllCompleted.loadSum) {
clearInterval(timer)
timer = null
callback(CommonPage, childLen)
}
}, 1000)
}
})
}
將生成pdf步驟作為回調(diào)函數(shù)放在上述函數(shù)里
commonPageLoadFinish(generatePdf)
?那在組件中如何監(jiān)聽圖片是否加載完成呢?按照以下代碼來寫
onMounted(() => {
nextTick(() => {
// commonRef.value 為某dom的refs
// 被動檢測是否有圖片, 無則直接進入主邏輯
const img = commonRef.value.querySelectorAll('img');
if (!img.length) return methods.successCallback();
let imgSum = 0;
asyncImgCompLoad(img).then((res) => {
res.forEach(() => imgSum++);
// 設(shè)置圖片總數(shù)量和加載數(shù)量
setImageAllCompleted({ sum: isImageAllCompleted.sum + img.length, loadSum: imgSum + isImageAllCompleted.loadSum });
})
})
})
async asyncImgCompLoad(imgList) {
const promiseList = []
for await (const item of imgList) {
promiseList.push(new Promise((res, rej) => {
// 參數(shù)沒有被賦值
if (!item.src) {
rej(false)
}
if (item.complate) {
res(true)
} else {
item.addEventListener('load', () => {
res(true)
})
// 圖片被賦值,但是賦的是錯誤的值
item.addEventListener('error', () => {
rej(false)
})
}
}))
}
return Promise.allSettled(promiseList)
}
?6、生成的pdf里,單頁底部有白邊?
在使用PDFjs插件時候,加入需要導(dǎo)出a4紙大小,那么很多童鞋就會將寬高固定設(shè)置為210mm, 297mm,但是實際上不是整數(shù),是小數(shù),所以獲取時按照下述方法獲取
// a4紙寬高
const A4Origin = {
width: PDF.internal.pageSize.getWidth(),
height: PDF.internal.pageSize.getHeight()
}
PDF分頁核心源碼
// a4紙寬高
const A4Origin = {
width: PDF.internal.pageSize.getWidth(),
height: PDF.internal.pageSize.getHeight()
}
const contentWidth = canvas.width;
/**
* html2canvas放大3.125倍時精度丟失導(dǎo)致多了2像素
* 3368: 高度285mm紙張html2canvas放大300dpi后像素
* 3366:正常實際高度
*/
const contentHeight = canvas.height <= 3368 ? 3366 : canvas.height;
const pageHeight = Math.round(contentWidth / A4Origin.width * A4Origin.height);
let leftHeight = contentHeight;
let position = 0;
const imgWidth = A4Origin.width;
const imgHeight = Math.ceil(A4Origin.width / contentWidth * contentHeight);
const pageData = canvas.toDataURL('image/jpeg', 1);
// 非首個分區(qū),得先addPage,因為不然會少一頁 && 大于某個范圍才新增一頁,避免因為浮點數(shù)計算精度造成多增一頁
if (!isFirstArea && leftHeight > 0) {
PDF.addPage()
}
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth + (isBrower() ? 0.62 : 0), imgHeight + (isBrower() ? 0.32 : 0));
position -= A4Origin.height;
leftHeight -= pageHeight
// 大于某個范圍才新增一頁,避免因為浮點數(shù)計算精度造成多增一頁
if (leftHeight > 0) {
PDF.addPage()
}
}
?有兩個地方可能童鞋們沒看懂,1、首先為啥非首個分區(qū),得先addPage呢?因為PDF默認(rèn)就有一頁,所以你能夠直接addImage而不出錯,然后后續(xù)PDF想要新增一頁,都得先addPage,這時候默認(rèn)背景顏色是白色的,然后再將canvas轉(zhuǎn)成圖片,貼到這白板上的,所以你看到PDF文檔里有白邊,那毫無疑問,就是貼的圖片沒占完那一頁,并且火狐瀏覽器和谷歌瀏覽器還有一些細(xì)微的差別所以你這就得一點一點微調(diào)來達(dá)到最佳顯示效果。2、為啥addIMage時,里面?zhèn)鞯膮?shù)不同呢?
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth + (isBrower() ? 0.62 : 0), imgHeight + (isBrower() ? 0.32 : 0));
?這就是瀏覽器差異問題,火狐瀏覽器不僅底部有白邊,側(cè)面也有白邊,相比之下谷歌要更兼容一些。
?7、pdf生成的File文件對象,想要先傳入oss,再通過服務(wù)端下載怎么實現(xiàn)?
這其實就涉及到大文件上傳技術(shù)了,因為pdf稍微大點可能都上百M,一般都不會一次性上傳完的,所以得做切片上傳,然后在服務(wù)端合并上傳到oss,最后將oss路徑地址返回給前端,前端通過這地址去下載。當(dāng)然具體的大文件上傳我就不寫在這篇博客了,下一篇博客我將著重講大文件上傳如何寫噢~
8、vue-fragment插件配合html2canvas使用有問題?
在@vue/composition-api環(huán)境下開發(fā),想使用fragment就得使用vue-fragment插件,但是搭配上html2canvas導(dǎo)出canvas會出現(xiàn)bug,就是只會渲染第一個fragment標(biāo)簽的dom,其他的都不會渲染,目前還是建議在@vue/composition-api環(huán)境下不使用fragment文章來源:http://www.zghlxwxcb.cn/news/detail-563441.html
--- 有問題可以隨時評論噢~喜歡的請點贊收藏啦 ---文章來源地址http://www.zghlxwxcb.cn/news/detail-563441.html
到了這里,關(guān)于【PDF】html/dom生成pdf的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!