大家好,
在這感謝潛水員大大,
幫忙解決了canvas 轉 pdf的問題。
之前是因為,有天突然發現highchart需要收費
所以想使用了chart.js去取代它,
才會想把highchart的這些功能給做出來
目前差在pdf跟svg的部分,已經完成90%,
就差兩個問題。
(1)pdf部分,在getTestJpg(){}裡,使用jpeg,
canvas可以存入pdf裡,但背景會被黑色,
還有圖片會被截斷的問題。
return ctx.toDataURL('image/jpeg', 1.0);
chart.js原始的圖,長這樣。
將jpeg改成jpg,背景正常,但是存不進pdf裡.....
(2)svg的部分,可以存檔,
可是開啟時,這個警告也一起存在裡面
頭好暈..
下面是全部的code
//pdf.js
**
* 產生 pdf 的基本工具
* 先用 addObject 寫入所需的物件
* 最後用 output 輸出
* 注意:這個檔案必須是 utf8 編碼,且換行為 lf
*/
function Pdf() {
this.idCount = 0;
this.data = [];
}
/**
* 加入一個 obj
*
* @param {object} dict
* @param {string|flase} stream
* @param {int|undefined} specId
*/
Pdf.prototype.addObject = function (dict, stream, specId) {
if (typeof stream === 'string') {
dict['Length'] = stream.length;
}
let arr = [];
let id;
if (specId === undefined) {
id = ++this.idCount;
} else {
id = specId;
}
arr.push(`${id} 0 obj`);
arr.push('<<');
for (let key in dict) {
arr.push(`/${key} ${dict[key]}`);
}
arr.push('>>');
if (typeof stream === 'string') {
arr.push('stream');
arr.push(stream);
arr.push('endstream');
}
arr.push('endobj');
this.data[id - 1] = arr.join('\n');
}
/**
* 預留 id
*
* @returns {int} 預留的 id
*/
Pdf.prototype.preserveId = function () {
return ++this.idCount;
}
/**
* 產生 pdf 的內容
*
* @param {int} rootId
* @returns {string} pdf內容
*/
Pdf.prototype.output = function (rootId) {
let header = '%PDF-1.4\n%§§';
let xrefArr = ['0000000000 65535 f '];
let pos = header.length + 3;
this.data.forEach((obj) => {
let s = pos.toString();
s = '0'.repeat(10 - s.length) + s;
xrefArr.push(`${s} 00000 n `);
pos += obj.length + 1;
});
return `${header}
${this.data.join('\n')}
xref
0 ${this.data.length + 1}
${xrefArr.join('\n')}
trailer
<<
/Size ${this.data.length + 1}
/Root ${rootId} 0 R
>>
startxref
${pos}
%%EOF
`;
}
/**
* 產生 pdf 的內容
*
* @param {int} rootId
* @returns {string} pdf 的 dataurl
*/
Pdf.prototype.toDataUrl=function(rootId) {
return 'data:application/pdf;base64,'+btoa_utf8(this.output(rootId));
}
/**
* btoa 的 utf8 版本
* @param {string} str 字串
* @returns {string} base64字串
*/
function btoa_utf8(str) {
const u8enc=new TextEncoder();
return btoa(Array.from(u8enc.encode(str), x=>String.fromCodePoint(x)).join(''));
}
export default Pdf;
//Chart.vue
<script setup>
import { onMounted,ref } from '@vue/runtime-core';
import Pdf from './assets/pdf'
import Chart from 'chart.js/auto';
const getPDFUrl = ref()
const getSVGUrl = ref()
onMounted(()=>{
//chart
const ctx = document.getElementById('myChart');
ctx.width = 200;
ctx.height = 200;
const myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
bezierCurve : false,
//必須關閉動畫,才能執行 toDataURL()
animations:false,
scales: {
y: {
beginAtZero: true
}
}
}
})
//chart base64
//使用jpeg,背景會變黑色,使用jpg又會抓不到
function getTestJpg() {
return ctx.toDataURL('image/jpeg', 1.0);
}
//base64 to 85
function base64ToBase85(base64) {
let arr = Array.from(atob(base64), x => x.codePointAt());
let padN = arr.length % 4 > 0 ? 4 - arr.length % 4 : 0;
while (padN-- > 0) {
arr.push(0);
}
//每4個一組
let result = '';
for (let i = 0, n = arr.length; i + 3 < n; i += 4) {
let H = (arr[i] << 8 | arr[i + 1]);
let L = (arr[i + 2] << 8 | arr[i + 3]);
let tmp = '';
for (let j = 0; j < 5; ++j) {
let t = (H % 85 << 16 | L % 85);
let r = t % 85; //對 85 取餘數
//除 85
H = (H - H % 85) / 85;
L = (L - L % 85) / 85 + (t - t % 85) / 85;
H += L >>> 16;
L&=0xffff;
tmp = String.fromCodePoint(33 + r) + tmp;
}
result += tmp;
}
return result;
}
//把圖片畫到 pdf 上
function drawToPdf(jpegBase64, opt) {
let jpegbase85 = base64ToBase85(jpegBase64.split('base64,')[1]); //轉成 base85
let pdf = new Pdf();
let catlogId = pdf.preserveId();
let pagesId = pdf.preserveId();
let firstPageId = pdf.preserveId();
let resourceId = pdf.preserveId();
let imgId = pdf.preserveId();
let contentId = pdf.preserveId();
pdf.addObject({
Type: '/Catalog',
Pages: `${pagesId} 0 R`,
}, false, catlogId);
pdf.addObject({
Type: '/Pages',
Kids: `[${firstPageId} 0 R]`,
Count: 1
}, false, pagesId);
pdf.addObject({
Type: '/Page',
Parent: `${pagesId} 0 R`,
Resources: `${resourceId} 0 R`,
Contents: `[${contentId} 0 R]`,
MediaBox: '[0 0 595.27559 841.88976]' //A4 大小
}, false, firstPageId);
pdf.addObject({
ProcSet: '[/PDF /ImageB ]',
XObject: `<< /Im1 ${imgId} 0 R >>`
}, false, resourceId);
pdf.addObject({
Type: '/XObject',
Subtype: '/Image',
Width: opt.width,
Height: opt.height,
ColorSpace: '/DeviceRGB',
BitsPerComponent: '8',
Filter: '[/ASCII85Decode /DCTDecode]'
}, jpegbase85+'~>', imgId);
pdf.addObject({}, `${opt.width} 0 0 ${opt.height} ${opt.x} ${opt.y} cm /Im1 Do`, contentId);
return pdf.toDataUrl(catlogId);
}
const jpgData = getTestJpg();
const pdfDataUrl = drawToPdf(jpgData, {
x:10,
y:500,
width: 300,
height: 300
});
getPDFUrl.value = pdfDataUrl
//將canvas轉成div
let canvasToDiv = `<img src="${jpgData}">`
//將div放入svg裡面
var svg = "<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'>" +
"<foreignObject width='100%' height='100%'>" +
"<div xmlns='http://www.w3.org/1999/xhtml' style='font-size:16px;font-family:Helvetica'>" +
canvasToDiv +
"</div>" +
"</foreignObject>" +
"</svg>";
//將svg轉成svg的base64
var urlSVG = "data:image/svg+xml;charset=utf-8,"+encodeURIComponent(svg);
getSVGUrl.value = urlSVG
})
</script>
<template>
<div class="canvasSize">
<canvas id="myChart"></canvas>
</div>
<button>
<a :href="getPDFUrl" download="myPDF.pdf">下載PDF</a>
</button>
<button>
<a :href="getSVGUrl" download="mySVG.svg">下載svg</a>
</button>
</template>
<style scoped>
.canvasSize{
width: 200px;
height: 200px;
}
</style>
jpeg 圖檔格式本身不支援透明
所以 toDataUrl 會把透明的地方當作黑色
解決的方式之一是塗滿白色後再畫上圖表
function getTestJpg() {
//另外建立一個 canvas
const cvs = document.createElement('canvas');
//讓長寬跟原本的一樣
cvs.width=ctx.width;
cvs.height=ctx.height;
//取得其 context
const ctx2 = cvs.getContext('2d');
//填滿白色
ctx2.fillStyle='#FFF';
ctx2.fillRect(0, 0, ctx.width, ctx.height);
//再把原來的圖表畫上去
ctx2.drawImage(ctx, 0, 0);
//回傳 data url
return cvs.toDataURL('image/jpeg', 1.0);
}
至於為什麼 image/jpg
會錯誤是因為本來就沒這種格式
所以會變成預設值,也就是 image/png
svg部分我還沒看