top of page
ค้นหา

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

  • รูปภาพนักเขียน: Sathit Jittanupat
    Sathit Jittanupat
  • 28 ม.ค. 2567
  • ยาว 4 นาที

ลองเขียนโค้ดสร้าง E-Tax Invoice (PDF/A-3) โดยใช้ JavaScript (open source PDF-LIB) ใน browser


ree

กรมสรรพากรกำหนดให้ผู้ประกอบการที่ต้องการใช้ใบกำกับภาษีอิเลคทรอนิคส์ ต้องส่งไฟล์ตามมาตรฐาน 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


ree

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


ree

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 ที่ได้ จะเห็นชื่อไฟล์ตามที่เราตั้งไว้

ree

ทั้งหมดคือกระบวนการสร้าง 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 บนเอกสาร


 
 
 

ความคิดเห็น


Post: Blog2_Post
  • Facebook

©2020 by Scraft On Cloud. Proudly created with Wix.com

bottom of page