iT邦幫忙

1

Canvas 轉pdf跟svg的疑問

大家好,
在這感謝潛水員大大,
幫忙解決了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的部分,可以存檔,
可是開啟時,這個警告也一起存在裡面

頭好暈..
/images/emoticon/emoticon06.gif

下面是全部的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>

1 個回答

3
淺水員
iT邦高手 2 級 ‧ 2021-11-29 21:44:32
最佳解答

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部分我還沒看

謝謝~成功了。
SVG部分,我也找到了,
是XML Parsing Error的問題,
原本的img少了一個關閉標籤

let canvasToDiv = `<img src="${jpgData}">`

要改成

let canvasToDiv = `<img src="${jpgData}" />`
淺水員 iT邦高手 2 級 ‧ 2021-11-30 09:30:19 檢舉

我在想要不要再確認一下專案需求
畢竟目前只是把點陣圖塞到 SVG 跟 PDF
失去了隨意縮放不失真的特性
不知道客戶要的是不是向量圖而不是點陣圖

原來如此!
我再確認看看,謝謝提醒~

我要發表回答

立即登入回答