ลองเขียนโค้ดสร้าง E-Tax Invoice (PDF/A-3) โดยใช้ JavaScript (open source PDF-LIB) ใน browser
กรมสรรพากรกำหนดให้ผู้ประกอบการที่ต้องการใช้ใบกำกับภาษีอิเลคทรอนิคส์ ต้องส่งไฟล์ตามมาตรฐาน PDF/A-3 สำหรับโปรแกรมเมอร์ทั่วไป ความยุ่งยากอยู่ที่ library ที่ใช้สร้างไฟล์ PDF/A-3 ที่แนะนำให้ใช้มีอยู่จำกัดแค่ openPDF (Java) หรือ iText (.NET, Java)
“PDF/A-3 คืออะไร” ดูคำอธิบายจาก getinvoice.net และ leceipt.com
ที่จริงแล้วข้อกำหนดของ PDF/A-3 เปิดเผยมานานกว่า 10 ปีแล้ว แต่ปัญหาอยู่ที่รายละเอียดซับซ้อนเกินไปจนคนทั่วไปอ่านไม่รู้เรื่อง จึงเหลือผู้พยายามแกะรายละเอียดไม่กี่ราย
หลังจากที่ได้เบาะแสสำคัญจาก Bancha ที่ช่วยหาชิ้นส่วนของโค้ดลึกลับและทดสอบให้ เราสามารถใช้ JavaScript สร้างไฟล์ PDF/A-3 จาก browser หรือใช้ Node.JS จาก server ด้วย library ที่เป็น open source ต่อไปนี้
html2canvas ใช้สำหรับแปลงฟอร์มเอกสารที่เป็น HTML ให้เป็นรูปภาพ
pdf-lib ใช้สำหรับสร้างไฟล์ PDF/A-3 โดยเอารูปภาพฟอร์มนั้นมาทำเป็น page
FileSaver เป็น optional สำหรับ save ไฟล์ pdf จาก browser ไม่ต้องใช้สำหรับ Node.JS ที่สามารถมี fs (File System)
สร้าง PDF โดยใช้ drawText, getForm & setText
การเขียนโปรแกรมสร้างไฟล์ PDF ออกมาเป็นฟอร์มเอกสารด้วยวิธีการดั้งเดิม เหมือนกับงานคราฟต์ จะใช้วิธีสั่งให้เขียนข้อความตามตำแหน่งที่ต้องการทีละข้อความ หรือสั่งให้วาดเส้นวาดกล่องสี่เหลี่ยมให้เป็นกรอบตาราง สำหรับผมคิดว่าเป็นเทคนิคที่ยุ่งยาก เสียเวลา ไม่มีความยืดหยุ่นในการปรับเปลี่ยน หากมีฟอร์มที่แตกต่างจำนวนมาก แทบไม่ต้องคาดหวังถึงความสวยงาม หรือความยืดหยุ่นในการปรับแต่งให้ถูกใจ
การสร้างเอกสารด้วยวิธีนี้ สำหรับ PDF/A-3 จำเป็นต้องแนบฟอนต์ที่ต้องใช้ในการ render ข้อความเข้าไปในไฟล์ PDF ด้วย สำหรับภาษาไทย คือ font “Sarabun New”
สร้าง PDF จาก HTML
การสร้างฟอร์มเอกสารด้วย HTML ทำได้ง่ายและสวยงามกว่าสำหรับโปรแกรมเมอร์รุ่นใหม่ สามารถใช้ร่วมกับ React หรือ Front End Tools ได้เหมือนกับการออกแบบหน้าจอต่าง ๆ
puppeteer
การแปลง HTML เป็น PDF ง่ายที่สุด คือ อาศัยคำสั่ง print ของ browser เพื่อ save เป็นไฟล์ แต่จะได้เป็น PDF ทั่วไปที่ไม่ใช่ PDF/A ดังนั้นจะต้องทำการปรับปรุงไฟล์ที่ได้อีกทีหนึ่ง
เราสามารถเขียนเป็นโค้ดที่ฝั่ง server โดยสั่งให้ puppeteer ทำตัวเสมือนเป็น browser ได้ไฟล์ PDF ออกมาได้
ข้อเสียของ puppeteer อยู่ที่ความช้าตอนเริ่มต้น เพราะต้องจำลอง browser (chrome headless) ขึ้นมาทำงานก่อน จึงเหมาะสำหรับการทำงานแบบ batch แปลงเอกสารคราวละหลายใบ
ผมจะไม่กล่าวถึงรายละเอียดตอนนี้ เพราะเป็นงานฝั่ง backend
html2canvas
ปกติ browser สามารถพิมพ์ page ใด ๆ ออกมาเป็น PDF ได้อยู่แล้ว แต่เราไม่สามารถเขียนโค้ดเพื่อควบคุมแบบ puppeteer ใน server
การสร้าง PDF/A ภายใน browser จึงใช้แล้วแปลง HTML ส่วนที่ต้องการเป็นรูปภาพใส่เข้าใน PDF แทน คล้ายกับการถ่ายรูปจากหนังสือทีละหน้ามาทำเป็น PDF โดยไม่ได้แปลงเป็นตัวอักษร
ข้อจำกัดของการใช้วิธีแนบรูป ทำให้คอนเทนต์ภายในไฟล์ไม่มีข้อความใด ๆ มีผลทำให้โปรแกรมรุ่นเก่าบางตัวที่อาจใช้วิธี extract ข้อความใน PDF เพื่อประมวลผลหรือค้นหาใช้ไม่ได้ แต่สำหรับคนทั่วไปรวมทั้ง AI ทั้งหลายต่างก็สามารถอ่านข้อความจากการแสดงผลของรูปภาพไม่มีปัญหา
ด้วยข้อจำกัดเกี่ยวกับความปลอดภัยของ browser หากมีรูปภาพประกอบเป็น cross origin image ที่อ้างถึง url ภายนอกใน tag<img .. > จะโดน block ไม่สามารถโหลดมาได้ ซึ่งวิธีการแก้คือ การทำ proxy api ให้เสมือน url ของภาพประกอบเหล่านั้นอยู่ใน origin เดียวกับ page ที่ใช้อยู่
ตัวอย่างโค้ด proxy api
const axios = require('axios')
const express = require('express')
const cors = require('cors');
const router = express.Router()
module.exports = routerconst _stream = (url, res) {
return axios.get(url, {responseType: 'stream'}).then(response => {
if (response.data)
return response.data.pipe(res)
res.status(response.status).send(res.statusText)
})
}
router.get(['/proxy'], cors(), async (req, res) => {
if (!req.query.url)
return res.status(400).send('No url specified');
if (!url.parse(req.query.url).host)
return res.status(400).send(`Invalid url specified: ${req.query.url}`)
if (req.query.responseType == 'blob') {
return _stream (req.query.url, res);
}
const data64 = await axios.get(url, {responseType: 'arraybuffer'})
.then( response => Buffer.from(response.data, 'binary').toString('base64'))
return res.send(`data:${response.headers['content-type']};base64,${data64}`);
})
สำหรับโค้ดใน browser ผมใช้ CDN html2canvas และ pdf-lib ซึ่งทำให้ได้ global entry html2canvas และ PDFLib สำหรับเรียกใช้ใน script
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js" integrity="sha512-BNaRQnYJYiPSqHHDb58B0yaPfCu+Wgds8Gp/gU33kqBtgNS4tSPHuGibyoeqMV/TJlSKda6FXzoEyYGjTe+vXA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js" integrity="sha512-z8IYLHO8bTgFqj+yrPyIJnzBDf7DDhWwiEsk4sY+Oe6J2M+WQequeGS7qioI5vT6rXgVRb4K1UVQC5ER7MKzKQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
เมื่อเรียกใช้ html2canvas จากฝั่ง client ก็สามารถระบุ options proxy เพื่อให้รูปภาพ logo และ ลายเซ็นในฟอร์มตามตัวอย่างด้านบนแสดงได้ถูกต้อง
ตัวอย่างโค้ด ใช้ document.querySelectorAll เลือกเฉพาะ block ที่ต้องการพิมพ์ หากมีฟอร์มต้องการพิมพ์หลายหน้า ก็จะมาทั้งหมด แล้วใช้ html2canvas แปลงหนึ่งภาพต่อหนึ่งหน้า
const html2pdf = () => {
const pages = document.querySelectorAll("div.pdf-page")
let pm = PDFLib.PDFDocument.create()
.then((pdf) => {
pdf.registerFontkit(window.fontkit)
return pdf
})
pages.forEach((pg) => {
const canvasOptions = {
proxy: '/ext-api/proxy/',
width: pg.scrollWidth + 20,
height: pg.scrollHeight + 20,
windowWidth: pg.scrollWidth,
windowHeight: pg.scrollHeight,
logging: false,
}
pm = pm.then((pdf) => {
return html2canvas(pg, canvasOptions)
.then((canvas) => {
const imgData = canvas.toDataURL('image/png');
return pdf.embedPng(imgData)
.then((img) => {
const pg = pdf.addPage()
const dims = img.scaleToFit(pg.getWidth() - 10, pg.getHeight() - 30)
pg.drawImage(img, {
x: 5,
y: pg.getHeight() - dims.height ,
width: dims.width,
height: dims.height
})
return pdf;
})
})
})
})
return pm;
}
สร้าง PDF/A-3 จาก PDF
เมื่อได้ PDF แล้ว ขั้นตอนต่อไป คือ การแนบส่วนประกอบอื่นที่จำเป็นตามมาตรฐานของ PDF/A-3 ดังนี้
Color Profile
ผมเลือกใช้ sRGB v2 profile หรือ sRGB2014.icc ต้อง download มาไว้เป็น static resource ที่ server ก่อน เพื่อให้สามารถเขียนโค้ดอ่านมาใช้ได้
ตัวอย่างโค้ด ดัดแปลงจากตัวอย่างของ PR setPrintProfile ของ necessarylion
const colorProfile = (pdfDoc) => {
// color profile
// setPrintProfile - https://github.com/Hopding/pdf-lib/pull/1512/commits/41436f23938bd04474635882f7e7d4096b743805
// https://www.color.org/srgbprofiles.xalter#v2
const url = "./resource/sRGB2014.icc"
const identifier = url.split('/').slice(-1)[0].split('.')[0] //'sRGB IEC61966-2.1'
const subType = 'GTS_PDFA1'
const info = indentifier
return $http.get(url, {responseType: 'arraybuffer'})
.then ((resp) => {
const iccBuffer = new Uint8Array(resp.data, 0, resp.data.byteLength)
const iccStream = pdfDoc.context.stream(iccBuffer, {Length: iccBuffer.length})
const outputIntent = pdfDoc.context.obj({
Type: 'OutputIntent',
S: subType,
OutputConditionIdentifier: PDFLib.PDFString.of(identifier),
Info: PDFLib.PDFString.of(info),
DestOutputProfile: pdfDoc.context.register(iccStream),
});
const outputIntentRef = pdfDoc.context.register(outputIntent);
pdfDoc.catalog.set(
PDFLib.PDFName.of('OutputIntents'),
pdfDoc.context.obj([outputIntentRef]),
);
return pdfDoc
})
}
XMP metadata
แนบข้อมูลที่จำเป็นเกี่ยวกับ PDF นี้ในรูปแบบของ XML และระบุ pdfaid ที่ต้องการ
const createMetadata = (xmldate, id, title, author, producer, creator, extension) => {
const pdfaSpec = ['3', 'U'];
return ` <?xpacket begin="" id="${id}"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.2-c001 63.139439, 2010/09/27-13:37:26 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:format>application/pdf</dc:format>
<dc:creator>
<rdf:Seq>
<rdf:li>${author}</rdf:li>
</rdf:Seq>
</dc:creator>
<dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">${title}</rdf:li>
</rdf:Alt>
</dc:title>
</rdf:Description>
<rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
<xmp:CreatorTool>${creator}</xmp:CreatorTool>
<xmp:CreateDate>${xmldate}</xmp:CreateDate>
<xmp:ModifyDate>${xmldate}</xmp:ModifyDate>
<xmp:MetadataDate>${xmldate}</xmp:MetadataDate>
</rdf:Description>
<rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<pdf:Producer>${producer}</pdf:Producer>
</rdf:Description>
<rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
<pdfaid:part>${pdfaSpec[0]}</pdfaid:part>
<pdfaid:conformance>${pdfaSpec[1]}</pdfaid:conformance>
</rdf:Description>${extension || ''} </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
`.trim();}
ต่อไปนี้เป็นโค้ดที่ผมใช้ประกอบส่วนต่าง ๆ เข้าด้วยกันกลายเป็น PDF/A-3 ตามตัวอย่างใน comment ของ necessarylion เช่นกัน
const pdfa3 = (pdfDoc, extension) => {
// https://github.com/Hopding/pdf-lib/issues/1183#issuecomment-1685078941
const createDate = new Date()
const producer= 'Account 4.0'
const creator = producer
const author = $parse('info.staff || _sys.owner')(docdata)
const docId = docdata._name
const = PDFLib.PDFHexString.of(docdata._id.$oid)
pdfDoc.context.trailerInfo.ID = pdfDoc.context.obj([id, id])
pdfDoc.setTitle(docId)
pdfDoc.setAuthor(author)
pdfDoc.setCreationDate(createDate);
pdfDoc.setModificationDate(createDate);
pdfDoc.setCreator(creator);
pdfDoc.setProducer(creator);
const xmldate = createDate.toISOString().split('.')[0] + 'Z';
const metadataXML = createMetadata(xmldate, id, docId, author, producer, creator, extension)
const metadataStream = pdfDoc.context.stream(metadataXML, {
Type: 'Metadata',
Subtype: 'XML',
Length: metadataXML.length,
});
const metadataStreamRef = pdfDoc.context.register(metadataStream);
pdfDoc.catalog.set(PDFLib.PDFName.of('Metadata'), metadataStreamRef);
return pdfDoc
}
const html2pdfa3 = () => {
return html2pdf()
.then(pdfa3)
.then(colorProfile)
}
Preview PDF
โค้ดสำหรับแสดง PDF ที่สร้างขึ้นมาได้ โดยไม่จำเป็นต้องบันทึกเป็นไฟล์ก่อน ทำได้โดยแปลงเป็น dataurl และใช้ <iframe> ดังนี้
const previewPDF = (pdf) => {
return pdf.saveAsBase64({ dataUri: true })
.then(function(url) {
const pdfWindow = window.open("")
const content = `<iframe width="100%" height="100%" src="${url}"></iframe>`
setTimeout(() => {
pdfWindow.document.write(content)
pdfWindow.document.title = docdata._name
})
return pdf ;
})
}
Save as PDF
ข้อจำกัดของการแสดง Preview ก่อน ถึงแม้ว่าจะมีปุ่มให้ download เพื่อบันทึกเป็นไฟล์ แต่จะไม่สามารถกำหนดชื่อไฟล์ให้เป็นชื่อตามเลขที่เอกสารได้
ดังนั้นอาจเลือกใช้วิธีสั่งให้ save เป็นไฟล์ โดยใช้ FileSaver ช่วย ทำให้ browser สามารถใช้คำสั่ง saveAs ได้
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js" integrity="sha512-Qlv6VSKh1gDKGoJbnyA5RMXYcvnpIqhO++MhIM2fStMcGT9i2T//tSwYFlcyoRRDcDZ+TYHpH8azBBCyhpSeqw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
const savePDF = (pdf) => {
return pdf.save()
.then(function (pdfBytes) {
const pdfname = docdata._name.replace(/[\/\\\:]/g,'_') + ".pdf"
const blb = new Blob([pdfBytes], {type: 'application/pdf'})
window.saveAs(blb, pdfname)
return pdf;
})
}
PDF/A-3 Validation
สามารถตรวจสอบว่าไฟล์ PDF มีองค์ประกอบขั้นต่ำครบตามมาตรฐานของ PDF/A-3 หรือไม่ โดยใช้ veraPDF
E-Tax Invoice
กรณีของ E-Tax Invoice มีการกำหนดเงื่อนไขให้แนบไฟล์ XML ตามมาตรฐานของ ETDA ขมธอ. 3–2560 เวอร์ชัน 2.0 เพิ่มในไฟล์ PDF ด้วย
XMP Extension
ภายใน XMP metadata จะต้องเพิ่ม block ส่วนที่กำหนด Electronic Tax Invoice PDFA Extension Schema ระบุข้อมูลที่สำคัญ 3 ค่า
DocumentFileName ชื่อไฟล์ XML เช่น ETDA-invoice.xml
DocumentType ประเภทเอกสาร เช่น Tax Invoice, Debit Note, Credit Note ดูภาคผนวก ข.2 ใน ETDA ขมธอ. 3–2560
Version ระบุเป็น 2.0
const etaxExtension = (doctype, filename) => {
const documentType = doctype // "Tax Invoice";
const documentFileName = filename // "ETDA-invoice.xml";
const version = "2.0";
return ` <rdf:Description xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/" xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#" xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#" rdf:about="">
<pdfaExtension:schemas>
<rdf:Bag>
<rdf:li rdf:parseType="Resource">
<pdfaSchema:schema>Electronic Tax Invoice PDFA Extension Schema</pdfaSchema:schema>
<pdfaSchema:namespaceURI>urn:etda:uncefact:data:standard:Invoice_CrossIndustryInvoice:2#</pdfaSchema:namespaceURI>
<pdfaSchema:prefix>rsm</pdfaSchema:prefix>
<pdfaSchema:property>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentFileName</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>Name of the embedded XML invoice file</pdfaProperty:description>
</rdf:li>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentType</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>Type of the document</pdfaProperty:description>
</rdf:li>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>Version</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>Version of the ETDA XML data</pdfaProperty:description>
</rdf:li>
</rdf:Seq>
</pdfaSchema:property>
</rdf:li>
</rdf:Bag>
</pdfaExtension:schemas>
</rdf:Description>
<rdf:Description xmlns:rsm="urn:etda:uncefact:data:standard:Invoice_CrossIndustryInvoice:2#" rdf:about="">
<rsm:DocumentFileName>${documentFileName}</rsm:DocumentFileName>
<rsm:DocumentType>${documentType}</rsm:DocumentType>
<rsm:Version>${version}</rsm:Version>
</rdf:Description>
`.trim();
}
const html2etaxPDF = (doctype, filename) => {
return html2pdf()
.then((pdf) => {
const extension = etaxExtension (doctype, filename)
return pdfa3(pdf, extension)
})
.then(colorProfile)
}
XML attachment
มาถึงส่วนสำคัญที่สุด เป็นข้อมูลที่กรมสรรพากรต้องการเอาไปประมวลผล การสร้างข้อมูล XML ตามรูปแบบที่กำหนด ผมจะยกไปกล่าวถึงเฉพาะอีกภาคหนึ่ง
เนื่องจากการสร้างไฟล์ PDF ใน browser ไม่สามารถสร้าง temp file เอาไว้ก่อนได้ จะต้องสร้างกลางอากาศใน memory สมมติว่าได้เป็น text ตามตัวอย่างด้านล่าง
const xmlString = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:TaxInvoice_CrossIndustryInvoice xmlns:ram="urn:etda:uncefact:data:standard:TaxInvoice_ReusableAggregateBusinessInformationEntity:2" xmlns:rsm="urn:etda:uncefact:data:standard:TaxInvoice_CrossIndustryInvoice:2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:etda:uncefact:data:standard:TaxInvoice_CrossIndustryInvoice:2 file:../data/standard/TaxInvoice_CrossIndustryInvoice_2p0.xsd">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID schemeAgencyID="ETDA" schemeVersionID="v2.0">ER3-2560</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>RDTIV0575526000058001</ram:ID>
<ram:Name>ใบกำกับภาษี</ram:Name>
<ram:TypeCode>388</ram:TypeCode>
<ram:IssueDateTime>2016-09-12T19:19:25.0</ram:IssueDateTime>
<ram:PurposeCode>TIVC01</ram:PurposeCode>
<ram:CreationDateTime>2016-09-12T15:51:26.0</ram:CreationDateTime>
<ram:IncludedNote>
<ram:Subject>หมายเหตุ</ram:Subject>
</ram:IncludedNote>
</rsm:ExchangedDocument>
</rsm:TaxInvoice_CrossIndustryInvoice>`;
เราสามารถเขียนโค้ดเพื่อเก็บ XML ไว้ใน PDF ให้เป็นเสมือนไฟล์ที่ซ่อนอยู่ได้
ชื่อไฟล์ XML ไม่จำเป็นต้องใช้ตามตัวอย่าง แต่ต้องระบุให้ตรงกับ ชื่อไฟล์ใน XMP extension ก่อนหน้านั้น
const enc = new window.TextEncoder()
const createDate = new Date()pdfDoc.attach(enc.encode(xmlString), 'ETDA-invoice.xml', {
mimeType: 'text/xml',
description: 'Tax Invoice',
creationDate: createDate,
modificationDate: createDate,
afRelationship: 'Alternative',
})
สามารถตรวจสอบ attachment โดยใช้ FireFox browser, Adobe Acrobat Reader หรือ Foxit PDF Reader เปิดไฟล์ PDF ที่ได้ จะเห็นชื่อไฟล์ตามที่เราตั้งไว้
ทั้งหมดคือกระบวนการสร้าง E-Tax Invoice ตามมาตรฐาน PDF/A-3 โดยใช้ JavaScript ใน browser จนสำเร็จ
XML ขมธอ. 3–2560
ยังเหลืองานส่วนสำคัญที่จะทำให้เป็นไฟล์ E-Tax Invoice คือการแกะโครงสร้างข้อมูล XML ตามข้อกำหนด ขมธอ. 3–2560 เพื่อให้สามารถส่งไปประทับเวลากับ ETDA หรือ ส่งไป sign กับ Service Provider
โปรดติดตามมหากาพย์ E-Tax Invoice ตอนสร้างไฟล์ XML ต่อไป
อ้างอิง
(1) โค้ดต้นแบบ C# จาก ETDA/e-TaxInvoice-PDFgen
Color Profile โค้ดต้นแบบยังไม่สมบูรณ์ ไม่ได้ระบุ SubType GTS_PDFA1 ทำให้ verify PDF/A-3 ไม่ผ่าน
XMP Metadata ใช้ตัวอย่างตามนั้น
Embed Font ไม่ต้องมี เนื่องจากไม่ได้ใช้วิธี drawText ถ้าต้องการแนบ (เช่น สร้างด้วย puppeteer ฝั่ง server) ดูตัวอย่างโค้ด embed Font and Measure Text แนะนำให้ใช้ th-sasabun-new
XML attachment อาศัยแกะจากโค้ดตัวอย่าง ที่นี่ และ ที่นี่ พบว่าต้องระบุ options {afRelationship: 'alternative'} ทำตามแล้ว verify PDF/A-3 ผ่าน
(2) comment จาก necessarylion ใน pdf-lib แนะนำวิธีสร้าง PDF/A-3 เป็นลายทางขุมทรัพย์เพียงชิ้นเดียวที่เจอ ยืนยันว่าทำได้ กลายเป็นความหวังให้เริ่มต้นทดลอง
(3) บทความ Download HTML as a PDF in React แนวทางนี้อาจใช้ไม่ได้ หากตีความเคร่งคัด เพราะผิดเงื่อนไข PDF/A-3u ที่ไม่สามารถค้นหา(search) และคัดลอก(copy) ข้อความที่เป็น Unicode บนเอกสาร
コメント