top of page
ค้นหา
รูปภาพนักเขียนSathit Jittanupat

Create an E-Tax Invoice (PDF/A-3) using JavaScript

ลองเขียนโค้ดสร้าง 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 บนเอกสาร


ดู 5 ครั้ง0 ความคิดเห็น

โพสต์ล่าสุด

ดูทั้งหมด

コメント


Post: Blog2_Post
bottom of page