大家好,
我想請問一下,
我想做一個存檔的按鈕,按下後會將chart.js的圖,
export成一個PDF檔。(之後還要做一個按鈕,將chart.js的圖存成SVG檔)
我爬了一些文,全部都是利用jsPDF這個套件去製作。
好處是方便、快速。
但是有個問題,這個套件大小要708KB,而我的專案總共也才1.5MB.....
為了這一個小小的功能,專案大小要多50%,有些不符合成本。
如果我想用原生的JS做出這個功能,要怎麼做呢?
還是有更方便的方法?
感謝
這是我目前的code(目前功能只有存出jpg png)
<script setup>
import { onMounted } from '@vue/runtime-core';
import Chart from 'chart.js/auto';
onMounted(()=>{
const ctx = document.getElementById('myChart');
const myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)'
],
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
})
//下載
const downloadBtn = document.getElementById("download_a")
downloadBtn.addEventListener('click', function(){
var url_base64jp =ctx.toDataURL("image/jpg");
var a = document.getElementById("download_a");
a.href = url_base64jp;
});
})
</script>
<template>
<div class="outSide">
<canvas id="myChart"></canvas>
</div>
<a id="download_a"
download="ChartImage.jpg"
href=""
title="Descargar Gráfico">下載</a>
</template>
<style scoped>
.outSide{
max-width: 700px;
}
#myChart{
width: 100%;
height: 100%;
}
</style>
直接用 js 而不使用函式庫的話
以下是一個範例
這會把一張 jpeg 畫在 pdf 上
並在網頁上的 iframe 呈現
html
<iframe width="800" height="600" frameborder="0"></iframe>
<script src="pdf.js"></script>
<script src="index.js"></script>
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(''));
}
index.js
/**
* 產生測試圖片
* 一個 200 x 200 中間畫圓的 jpg
*
* @returns {string} base64 data url
*/
function getTestJpg() {
let cvs = document.createElement('canvas');
cvs.width = 200;
cvs.height = 200;
let ctx = cvs.getContext('2d');
ctx.fillStyle = "rgb(255,255,255)";
ctx.fillRect(0, 0, 200, 200);
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.stroke();
return cvs.toDataURL('image/jpeg', 1.0);
}
/**
* 把圖片畫到 pdf 上
* 並產生 pdf 的 dataurl
*
* @param {string} jpegBase64 jpeg的dataurl
* @param {object} opt 用來指定位置與長寬 {x:x座標, y:y座標, width:寬度, height:高度}
* @returns {string} pdf 的 dataurl
*/
function drawToPdf(jpegBase64, opt) {
let jpegbase85 = base64ToBase85(jpegBase64.split('base64,')[1]); //轉成 base85
/**
* pdf 的結構請參考
* https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf
*/
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);
/**
* 把 base64 轉 base85
* @param {string} base64 base64字串
* @returns {string} base85字串
*/
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;
}
}
const jpgData = getTestJpg();
const pdfDataUrl = drawToPdf(jpgData, {
x: 100,
y: 600,
width: 200,
height: 200
});
document.querySelector('iframe').src = pdfDataUrl;
淺水員
是否可以允許我將這段程式碼收錄到我的論壇內?
我會保留你的名字跟出處。
可以,有需要的話也可以修改。
㊣浩瀚星空㊣
剛剛我有再修過,修正後的 Pdf.addObject 可以不用跟 Pdf.preserveId 的順序一樣,可以任意對調了。
好的。其實我是等你認可。我才會將程式碼移轉。
已將大作放入我的論壇上了。
感謝啦!!
補上兩個範例
如果原發問者能轉成 svg
那麼有可能像範例二的方式直接用路徑的方式畫出圖表
而不是透過點陣圖的方式
(不過還有文字的部分要處理,會比較麻煩)
/**
* 空白頁範例(最簡單的 pdf)
*
* @returns {string} pdf 的 dataurl
*/
function simplePdf() {
let pdf = new Pdf();
let catlogId = pdf.preserveId();
let pagesId = pdf.preserveId();
let firstPageId = 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: `<< >>`,
Contents: `[]`,
MediaBox: '[0 0 595.27559 841.88976]' //A4 大小
}, false, firstPageId);
return pdf.toDataUrl(catlogId);
}
/**
* 向量圖範例
*
* @returns {string} pdf 的 dataurl
*/
function drawPathPdf() {
let pdf = new Pdf();
let catlogId = pdf.preserveId();
let pagesId = pdf.preserveId();
let firstPageId = 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: `<< >>`,
Contents: `[${contentId} 0 R]`,
MediaBox: '[0 0 595.27559 841.88976]' //A4 大小
}, false, firstPageId);
/**
* 以下是一個 Content Stream
* 用 PostScript 畫圖形
* 理論上 svg 的路徑都可以轉成對應的 PostScript
*/
pdf.addObject({}, [
'10 10 m 60 110 l 110 10 l s', //空心三角形
'130 10 m 180 110 l 230 10 l f', //實心三角形
'0 0 1 rg 250 10 m 300 110 l 350 10 l f', //實心三角形(藍色)
'1 0 0 RG 370 10 m 420 110 l 470 10 l s' //空心三角形(紅色)
].join(' '), contentId);
return pdf.toDataUrl(catlogId);
}
謝謝你~~~~~~~~~
能在問一下,我的圖片還是存不進去pdf裡面
可以下載PDF,不過開啟時是空白的。
我從console.log(pdfDataUrl)的網址連進去
開啟時也是空白的,
是我哪邊寫錯了呢?
<body>
<canvas id="myCanvas"></canvas>
<button id="myBtn">下載</button>
<script src="./pdf.js"></script>
<script>
var myCanvas = document.getElementById("myCanvas")
myCanvas.width = 200;
myCanvas.height = 200;
var ctx = myCanvas.getContext("2d")
ctx.fillStyle = "rgb(255,255,255)";
ctx.fillRect(0, 0, 200, 200);
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.stroke();
var jpgData = myCanvas.toDataURL('image/jpeg', 1.0);//jpegBase64
//把圖片畫到 pdf 上
function drawToPdf(jpegBase64, opt) {
let jpegbase85 = 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);
//因為想直接用url下載 所以就把function base64ToBase85 刪除了
}
const pdfDataUrl = drawToPdf(jpgData, {
x: 100,
y: 600,
width: 200,
height: 200
});
console.log(pdfDataUrl);//之後直接拿這個去下載pdf
</script>
應該用 base85 的地方卻用 base64 當然會出錯
原先的 function base64ToBase85
放在那邊並不妨礙回傳值是 data url
可以直接放到超連結
淺水員感謝你~~~
可以再請教你兩個問題嗎
因為文章太長了 我再開一個問題
能再麻煩你嗎?感謝~~
提供兩個參考
1.這個jspdf的範例只有297KB
2.另一個pdfmake,我不知道多大,麻煩自己看
個人覺得
jspdf 的作者如果可以少寫一列程式碼就不會多寫
如果硬要減少檔案大小
你可以去讀jspdf 的原始碼
然後刪掉一些你用不到的功能