EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / ออกใบเสนอราคา PDF ภาษาไทยจาก HTML ด้วย Chromium และกับดักที่เจอ
Linux

ออกใบเสนอราคา PDF ภาษาไทยจาก HTML ด้วย Chromium และกับดักที่เจอ

โดย Ton-Or ·31 ส.ค. 2026·อ่าน 9 นาที
ออกใบเสนอราคา PDF ภาษาไทยจาก HTML ด้วย Chromium และกับดักที่เจอ

ออก PDF จาก HTML ฟังดูเป็นงานครึ่งวัน แต่ของจริงมันไปตายที่รายละเอียดรอบนอกทั้งนั้น — เบราว์เซอร์ที่หาไม่เจอ ฟอนต์ไทยที่กลายเป็นสี่เหลี่ยม ชื่อไฟล์ที่มีอักขระต้องห้าม และเลขเอกสารที่ดันมี slash อยู่ข้างใน

พอดีผมทำระบบออกเอกสารให้งานหนึ่ง เป็นระบบที่ต้องออกใบเสนอราคากับใบส่งของให้ หลายบริษัทในเครือ ซึ่งแต่ละบริษัทมีหัวกระดาษ สี ฟอนต์ และรูปแบบเลขเอกสารของตัวเอง คนละเทมเพลตกันหมด โจทย์คือกดปุ่มเดียวแล้วได้ PDF A4 ที่พิมพ์ออกมาแล้วหน้าตาเหมือนที่เห็นบนจอเป๊ะ ผมเลือกทางที่ตรงที่สุดคือให้ Chromium ตัวเดียวกับที่เรนเดอร์หน้าเว็บนั่นแหละ พิมพ์ออกมาเป็น PDF ให้เลย ฟังดูง่ายครับ แต่ผมไปเสียเวลากับกับดักรอบนอกหลายวัน เลยเก็บมาเล่าไว้

ชื่อบริษัท ชื่อลูกค้า รูปแบบเลขเอกสาร และชื่อไฟล์ทั้งหมดในบทความเป็นตัวอย่างที่ผมสมมติขึ้น โครงสร้างระบบกับโค้ดคือของจริง

ทำไมผมเลือกให้เบราว์เซอร์พิมพ์ แทนที่จะวาด PDF เอง

ไลบรารีสร้าง PDF ตรงๆ นั้นมีเยอะและเร็วกว่ามาก แต่พอเจอโจทย์เอกสารไทยที่ต้องมีตารางยืดหด หัวกระดาษต่างกันสี่แบบ ฟอนต์ไทยที่ต้องวางสระบนล่างให้ถูก และ layout ที่ลูกค้าขอปรับได้เอง มันกลายเป็นงานเขียนเอนจินจัดหน้าใหม่ทั้งตัว ในขณะที่ถ้าใช้ HTML กับ CSS ผมได้ทั้งหมดนั้นฟรีอยู่แล้ว และที่สำคัญคือ ตัวอย่างบนหน้าเว็บกับไฟล์ที่ออกมา ใช้โค้ดชุดเดียวกัน ไม่มีทางเพี้ยนกันได้เพราะมันคือ blade ตัวเดียวกัน

เส้นทางของหนึ่งใบbladeเทมเพลตต่อบริษัท->HTML ชั่วคราวฟอนต์ฝังมาแล้ว->Chromiumfull build->page.pdfA4 + เลขหน้า->ไฟล์ปลายทางชื่อผ่าน sanitize

กับดักแรก: Executable doesn't exist ทั้งที่ลง Chromium ไว้แล้ว

รันครั้งแรกก็เจอเลยครับ

browserType.launch: Executable doesn't exist at
C:\Users\<user>\AppData\Local\ms-playwright\chromium_headless_shell-1217\chrome-win\chrome-headless-shell.exe
Looks like Playwright was just installed or updated.
Please run the following command to download new browsers:
    npx playwright install

ที่งงคือในเครื่องมี Chromium อยู่จริง เปิดใช้งานอย่างอื่นได้ปกติ ผมไปไล่ดูโฟลเดอร์ก็เจอว่ามันมี chromium-1187 อยู่ แต่ตัวที่ Playwright เรียกหาชื่อ chromium_headless_shell-1217 ซึ่งไม่มี

สาเหตุคือ Playwright รุ่นใหม่แยกเบราว์เซอร์ออกเป็นสองตัว ตัวเต็มกับ headless shell ที่ตัดส่วน UI ทิ้งเพื่อให้เบาและเร็วขึ้น พอเราสั่ง launch() แบบ headless เป็นค่าเริ่มต้น มันจะไปหยิบ headless shell ก่อนเสมอ ทีนี้ถ้าเครื่องเราลงเบราว์เซอร์ไว้ตั้งแต่ Playwright รุ่นเก่า หรือลงมาเป็นตัวเต็มอย่างเดียว ก็จะไม่มี shell ตัวนั้น แล้วมันไม่ยอมถอยไปใช้ตัวเต็มให้ด้วย

chrome-headless-shell

ตัวที่ Playwright เรียกหาโดยปริยาย

  • เล็กกว่า เปิดเร็วกว่า
  • ดาวน์โหลดแยก คนละ revision กับตัวเต็ม
  • เครื่องที่ลงข้ามรุ่นมามักไม่มี
  • ไม่มีแล้ว launch พังทันที ไม่ fallback
chromium ตัวเต็ม

ตัวที่เรามีอยู่จริงในเครื่อง

  • อยู่ที่ chromium-<rev>/chrome-win/chrome.exe
  • เรนเดอร์ผลเหมือนกันทุกประการ
  • ชี้ตรงได้ด้วย executablePath
  • หนักกว่านิดหน่อย แต่ไม่กระทบงานออกเอกสาร

ทางแก้ที่ผมใช้คือไม่ปล่อยให้มันเลือกเอง ผมเขียนฟังก์ชันไล่หาตัวเต็มที่มีจริงในเครื่องแล้วชี้ executablePath ให้ตรงๆ ไปเลย

// tools/render.cjs — หา chromium ตัวเต็มที่มีจริง แล้วชี้ให้ launch()
const fs   = require('fs');
const path = require('path');
const { chromium } = require(process.env.PW_PKG || 'playwright');

// เครื่องนี้เคยลงข้ามรุ่นมาหลายรอบ เลยไล่ทีละ revision ที่เป็นไปได้
const REVS = ['1223', '1217', '1208', '1187'];

function fullChromeExe() {
  const root = process.env.PLAYWRIGHT_BROWSERS_PATH
            || path.join(process.env.LOCALAPPDATA || '', 'ms-playwright');
  for (const rev of REVS) {
    for (const rel of [`chromium-${rev}/chrome-win/chrome.exe`,      // windows
                       `chromium-${rev}/chrome-linux/chrome`]) {      // linux
      const p = path.join(root, rel);
      if (fs.existsSync(p)) return p;
    }
  }
  return null;   // ไม่เจอก็ปล่อยให้ playwright จัดการเอง แล้วค่อยอ่าน error
}

async function launch() {
  const exe = fullChromeExe();
  if (!exe) console.warn('[WARN] ไม่พบ chromium ตัวเต็ม จะปล่อยให้ playwright เลือกเอง');
  return chromium.launch({
    headless: true,
    ...(exe ? { executablePath: exe } : {}),
    args: ['--no-sandbox', '--font-render-hinting=none'],  // hinting=none ให้ตัวอักษรคมเท่ากันทุกเครื่อง
  });
}
อย่าแก้ด้วยการสั่ง npx playwright install บนเครื่องโปรดักชัน

คำแนะนำที่ error พ่นออกมาคือให้ติดตั้งเบราว์เซอร์ใหม่ ซึ่งบนเครื่องพัฒนาก็ทำได้ แต่บนเซิร์ฟเวอร์ที่คุมเวอร์ชันไว้ การสั่งติดตั้งใหม่แปลว่า ดาวน์โหลดเบราว์เซอร์อีกชุดหนึ่งเป็นหลายร้อยเมกะไบต์ และอาจได้ revision ใหม่ที่เรนเดอร์ต่างจากที่เทสต์ไว้ ผมเลือกชี้ไปที่ตัวที่มีอยู่แล้วดีกว่า ควบคุมได้ว่าเอกสารทุกใบออกจากเบราว์เซอร์ตัวเดียวกันจริงๆ

ใครที่ทำฝั่ง Python ก็ปัญหาเดียวกันและแก้เหมือนกันครับ

# tools/render.py — เวอร์ชัน Python ปัญหาเดียวกัน วิธีแก้เดียวกัน
import os
from pathlib import Path
from playwright.sync_api import sync_playwright

REVS = ["1223", "1217", "1208", "1187"]

def full_chrome() -> str | None:
    root = Path(os.environ.get("PLAYWRIGHT_BROWSERS_PATH")
                or Path(os.environ["LOCALAPPDATA"]) / "ms-playwright")
    for rev in REVS:
        for rel in (f"chromium-{rev}/chrome-win/chrome.exe",
                    f"chromium-{rev}/chrome-linux/chrome"):
            p = root / rel
            if p.is_file():
                return str(p)
    return None

def render(html_path: str, out_pdf: str) -> None:
    with sync_playwright() as pw:
        exe = full_chrome()
        browser = pw.chromium.launch(executable_path=exe) if exe else pw.chromium.launch()
        page = browser.new_page()
        page.goto(Path(html_path).as_uri(), wait_until="networkidle")
        page.emulate_media(media="print")          # ให้ @media print มีผลจริง
        page.pdf(path=out_pdf, format="A4", print_background=True,
                 margin={"top": "12mm", "bottom": "14mm", "left": "12mm", "right": "12mm"})
        browser.close()

จัด A4 กับแบ่งหน้าให้ตารางไม่ขาดกลางแถว

ค่าเริ่มต้นของ page.pdf() จะไม่พิมพ์พื้นหลังให้ ต้องเปิด printBackground เอง ไม่งั้นแถบสีหัวตารางหายหมด และต้องสั่ง emulateMedia('print') ก่อนด้วย ไม่งั้น CSS ใน @media print ไม่มีผล

ความสูง .sheet = hA4 สูง 1122 px (297mm ที่ 96dpi)h น้อยกว่าหรือเท่ากับ 1122scale = 1จบหน้าเดียวพอดีh มากกว่า 1122scale = 1122 / hย่อได้ไม่เกิน 40%ต่ำสุด 0.60ย่อสุด 0.60 ยังไม่พอscale = 1แบ่งหน้าจริง + เลขหน้าเมื่อ h มากกว่า 1122 / 0.60รอ document.fonts.ready ก่อนวัด h ทุกครั้ง
ความสูงของ .sheet ตัดสินว่าจะย่อก่อน ถ้าเกิน A4 ให้ย่อได้ไม่เกิน 40% และถ้าย่อสุดแล้วยังไม่พอค่อยปล่อยแบ่งหน้าจริงพร้อมเลขหน้า
// เรนเดอร์หนึ่งใบ: ลองย่อให้จบหน้าเดียวก่อน ถ้าเกินจริงค่อยปล่อยให้แบ่งหน้า + ใส่เลขหน้า
const page = await (await launch()).newPage();
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle' });
await page.emulateMedia({ media: 'print' });
await page.evaluate(() => document.fonts.ready);        // รอฟอนต์โหลดจบก่อนวัดความสูง

const A4H = 1122;                                       // 297mm ที่ 96dpi
const h   = await page.$eval('.sheet', el => el.scrollHeight);
const fit = h <= A4H ? 1 : Math.max(0.60, A4H / h);     // ย่อได้ไม่เกิน 40%

const opts = {
  path: outPdf, format: 'A4', printBackground: true, scale: Number(fit.toFixed(3)),
  margin: { top: '12mm', bottom: '14mm', left: '12mm', right: '12mm' },
};

if (fit === 0.60 && h > A4H / 0.60) {                   // ย่อสุดแล้วยังไม่พอ = ต้องแบ่งหน้าจริง
  opts.scale = 1;
  opts.displayHeaderFooter = true;
  opts.headerTemplate = '<div></div>';                  // ต้องใส่ ไม่งั้นมันโชว์หัวเริ่มต้นของ chrome
  opts.footerTemplate =
    '<div style="width:100%;font-size:9px;padding:0 12mm;text-align:right;color:#555">'
    + 'หน้า <span class="pageNumber"></span> / <span class="totalPages"></span></div>';
}
await page.pdf(opts);

ฝั่ง CSS ต้องบอกเบราว์เซอร์ด้วยว่าตรงไหนห้ามขาด ไม่งั้นแถวสินค้าจะถูกตัดครึ่งคาบรรทัด

@page { size: A4; margin: 0; }

.sheet        { width: 210mm; }
thead         { display: table-header-group; }   /* หัวตารางซ้ำทุกหน้าอัตโนมัติ */
tfoot         { display: table-footer-group; }
tr, .no-break { break-inside: avoid; }           /* ห้ามตัดกลางแถว */
.page-break   { break-before: page; }
.sign-block   { break-inside: avoid; }           /* บล็อกลายเซ็นห้ามหลุดไปคนละหน้ากับยอดรวม */

เรื่องฟอนต์ไทยนี่ผมย้ำเป็นพิเศษ ตอนพัฒนาในเครื่องมันสวยดีเพราะดึงฟอนต์จากอินเทอร์เน็ตได้ พอขึ้นเซิร์ฟเวอร์ที่ไม่มีทางออกเน็ต หรือมีแต่ช้ากว่า timeout ตัวอักษรไทยกลายเป็นสี่เหลี่ยมเปล่าทั้งใบ ทางแก้คือ ฝังฟอนต์มากับหน้าเลย อ้างเป็นไฟล์ในเครื่องผ่าน @font-face แล้วรอ document.fonts.ready ก่อนสั่งพิมพ์เสมอ อย่าพึ่งเน็ตตอนออกเอกสาร

sanitize ชื่อไฟล์ภาษาไทย ก่อนเขียนลงดิสก์

ชื่อไฟล์ที่ระบบสร้างมาจากเลขเอกสารบวกชื่อลูกค้า ซึ่งเป็นภาษาไทยและเป็นข้อมูลที่ผู้ใช้พิมพ์เอง แปลว่าอาจมีอะไรก็ได้อยู่ในนั้น อักขระที่ Windows ห้ามใช้ในชื่อไฟล์คือ \ / : * ? " < > | บวกกับอักขระควบคุมโค้ด 0 ถึง 31 และยังมีกฎอีกสองสามข้อที่คนลืมกันบ่อย

<?php
// app/Services/FileName.php — ทำให้ชื่อไฟล์ปลอดภัยทั้งบน Windows และ Linux
final class FileName
{
    // ชื่อสงวนของ Windows ใช้เป็นชื่อไฟล์ไม่ได้แม้จะมีนามสกุลต่อท้าย
    private const RESERVED = ['CON','PRN','AUX','NUL',
        'COM1','COM2','COM3','COM4','COM5','COM6','COM7','COM8','COM9',
        'LPT1','LPT2','LPT3','LPT4','LPT5','LPT6','LPT7','LPT8','LPT9'];

    public static function safe(string $name, string $ext = 'pdf'): string
    {
        // 1) อักขระต้องห้ามของ Windows + อักขระควบคุม -> ขีดล่าง
        $name = preg_replace('/[\\\\\/:\*\?"<>\|\x00-\x1F]/u', '_', $name);

        // 2) ยุบช่องว่างซ้ำ แล้วตัดหัวท้าย (ภาษาไทยเก็บไว้ตามเดิม ไม่ต้องถอดเป็นอังกฤษ)
        $name = trim(preg_replace('/\s+/u', ' ', $name));

        // 3) Windows ตัด จุด กับ ช่องว่าง ท้ายชื่อทิ้งเงียบๆ -> ชื่อเพี้ยนจากที่ตั้งใจ
        $name = rtrim($name, ". \t");

        // 4) ชื่อสงวน -> เติมขีดล่างกัน
        if (in_array(mb_strtoupper($name), self::RESERVED, true)) {
            $name = '_' . $name;
        }

        // 5) จำกัดความยาว "เป็นไบต์" เพราะไทยตัวละ 3 ไบต์ ext4 จำกัดที่ 255 ไบต์
        while (strlen($name) > 200) {
            $name = mb_substr($name, 0, mb_strlen($name) - 1);   // ตัดทีละอักขระ ไม่ตัดกลาง UTF-8
        }

        return ($name === '' ? 'document' : $name) . '.' . $ext;
    }

    /** ส่งดาวน์โหลดให้ชื่อไทยไม่กลายเป็นขยะบนเบราว์เซอร์เก่า */
    public static function disposition(string $filename): string
    {
        $ascii = preg_replace('/[^\x20-\x7E]/', '_', $filename);   // ชื่อสำรองแบบ ASCII
        return sprintf('attachment; filename="%s"; filename*=UTF-8\'\'%s',
            str_replace('"', '', $ascii), rawurlencode($filename));
    }
}

ข้อที่ 5 นี่คือจุดที่ผมโดนมาจริงครับ ตอนแรกผมตัดด้วย mb_substr ที่ 200 อักขระ แล้วชื่อไทยยาวๆ ก็ยังเขียนไฟล์ไม่ผ่านบน Linux เพราะ ext4 นับ ไบต์ ไม่ใช่อักขระ ภาษาไทยหนึ่งตัวกิน 3 ไบต์ ชื่อ 200 ตัวอักษรเลยกลายเป็น 600 ไบต์ เกินเพดาน 255 ไปไกล ต้องวนตัดจนความยาวเป็นไบต์เข้าเกณฑ์ และห้ามใช้ substr ธรรมดาเพราะมันตัดกลางตัวอักษร UTF-8 แล้วได้ชื่อเป็นขยะ

ระบบเลขเอกสาร: token กับตารางตัวนับ

แต่ละบริษัทในเครือต้องการรูปแบบเลขเอกสารของตัวเอง และที่สำคัญคือ เลขรันต้องแยกกัน ไม่ใช่รันต่อกันข้ามบริษัท ผมเลยเก็บรูปแบบเป็นสตริง token ไว้ที่ระเบียนบริษัท แล้วมีตารางตัวนับกลางตัวเดียว

-- ตัวนับกลาง หนึ่งแถว = หนึ่งบริษัท x หนึ่งชนิดเอกสาร x หนึ่งช่วงรีเซ็ต
CREATE TABLE series_counters (
  skey     VARCHAR(64) NOT NULL,   -- เช่น S1|QT|202608
  last_no  INT UNSIGNED NOT NULL DEFAULT 0,
  UNIQUE KEY uk_skey (skey)
) ENGINE=InnoDB;
<?php
// app/Services/DocNumber.php — สร้างเลขเอกสารตามรูปแบบของแต่ละบริษัท
final class DocNumber
{
    /** {T}=ชนิด · {YYYY}/{YY}=ปี ค.ศ. · {BE}/{BY}=ปี พ.ศ. · {MM}=เดือน · {R}/{R3}/{R4}=เลขรัน */
    public static function nextFor(Company $self, string $type, ?string $date = null): string
    {
        $ts  = strtotime($date ?? 'now');
        $fmt = $self->doc_format ?: '{T}{YYYY}{MM}{R4}';

        // ช่วงรีเซ็ต: มี {MM} = รายเดือน · มีปีอย่างเดียว = รายปี · ไม่มีเลย = รันต่อเนื่องไม่รีเซ็ต
        $period = str_contains($fmt, '{MM}')
            ? date('Ym', $ts)
            : (preg_match('/\{(YYYY|YY|BE|BY)\}/', $fmt) ? date('Y', $ts) : 'ALL');

        $skey = sprintf('S%d|%s|%s', $self->id, $type, $period);

        // นับแบบ atomic ใน transaction เดียว กันสองคนกดพร้อมกันแล้วได้เลขซ้ำ
        $no = DB::transaction(function () use ($skey) {
            DB::statement('INSERT INTO series_counters (skey, last_no) VALUES (?, 1)
                           ON DUPLICATE KEY UPDATE last_no = last_no + 1', [$skey]);
            return (int) DB::selectOne('SELECT last_no FROM series_counters WHERE skey = ?', [$skey])->last_no;
        });

        return strtr($fmt, [
            '{T}'    => $type,
            '{YYYY}' => date('Y', $ts),          '{YY}' => date('y', $ts),
            '{BE}'   => (int) date('Y', $ts) + 543,
            '{BY}'   => substr((string) ((int) date('Y', $ts) + 543), 2),
            '{MM}'   => date('m', $ts),
            '{R}'    => (string) $no,
            '{R3}'   => str_pad((string) $no, 3, '0', STR_PAD_LEFT),
            '{R4}'   => str_pad((string) $no, 4, '0', STR_PAD_LEFT),
        ]);
    }
}

รูปแบบที่ใช้จริงในระบบตัวอย่างมีสี่แบบ บริษัทแรกไม่ตั้งอะไรเลยก็ได้ค่าปริยาย {T}{YYYY}{MM}{R4} ออกมาเป็น QT2026080004 บริษัทที่สองใช้ {T}-AA{BY}{MM}-{R3} ได้ QT-AA6908-001 บริษัทที่สามใช้ {T}.BB.{BY}{MM}.{R3} ได้ QT.BB.6908.001 และบริษัทที่สี่ใช้ {T}-{BE}{MM}-CC{R3} ได้ QT-256908-CC001 ทั้งสี่แบบมีตัวนับแยกกันสนิทเพราะ skey มีรหัสบริษัทอยู่ข้างหน้า

4 แบบรูปแบบเลขเอกสารต่อเครือ
1 ตารางตัวนับกลางใช้ร่วมกันทุกบริษัท
9 ตัวอักขระต้องห้ามในชื่อไฟล์
255 ไบต์เพดานชื่อไฟล์บน ext4
A4ขนาดกระดาษที่บังคับใน page.pdf
0.60เพดานย่อก่อนยอมแบ่งหน้า

เคสจริง: slash ตัวเดียวในเลขเอกสาร ทำ path พังทั้งระบบ

ตอนเปิดใช้จริง มีบริษัทหนึ่งขอรูปแบบตามเอกสารกระดาษเดิมของเขาคือ {T}/{BE}/{MM}/{R3} ซึ่งได้เลขออกมาหน้าตาว่า QT/2569/08/001 ระบบสร้างเลขได้ปกติ บันทึกลงฐานข้อมูลได้ปกติ แต่พอกดออก PDF ปุ๊บ ไฟล์หายไปเฉยๆ ไม่มี error ให้ดูด้วยซ้ำในตอนแรก

สาเหตุตรงไปตรงมาครับ ชื่อไฟล์คือ QT/2569/08/001.pdf ซึ่งบน Linux นั้น slash ไม่ใช่ตัวอักษร มันคือ ตัวคั่นไดเรกทอรี ระบบเลยตีความว่าให้ไปเขียนไฟล์ชื่อ 001.pdf ในโฟลเดอร์ QT/2569/08/ ที่ไม่มีอยู่จริง บางจังหวะก็สร้างโฟลเดอร์ขยะทิ้งไว้ บางจังหวะก็เขียนไม่ผ่านเงียบๆ แล้วยังไม่พอ URL ที่ลิงก์ไปหาไฟล์ก็พังตามไปด้วยเพราะ slash ไปทำให้ route ตีความ path ผิด

sanitize เป็นตาข่ายรับ ไม่ใช่ทางแก้ที่ต้นเหตุ

ตัว sanitize ที่เขียนไว้ข้างบนจับ slash ได้อยู่แล้ว ชื่อไฟล์จะกลายเป็น QT_2569_08_001.pdf ซึ่งเขียนได้ไม่พัง แต่ผู้ใช้จะงงว่าทำไมเลขในเอกสารกับชื่อไฟล์ไม่ตรงกัน ผมเลยไปปิดที่ต้นทางเพิ่มด้วย คือ ห้ามใส่อักขระต้องห้ามลงในตัวรูปแบบเลขเอกสารตั้งแต่แรก ตรวจตอนบันทึกค่าตั้งค่าเลย ใครอยากได้หน้าตาแบบมีตัวคั่น ให้ใช้ขีดกลางหรือจุดแทน slash

การแก้ให้บริษัทที่ออกเลขผิดรูปแบบไปแล้ว ผมทำเป็นขั้นตอนตายตัวแบบนี้

  1. ตั้งรูปแบบใหม่ที่ไม่มีอักขระต้องห้ามให้บริษัทนั้นก่อน
  2. ลบตัวนับของบริษัทนั้นทิ้ง ด้วยเงื่อนไข skey LIKE 'S{id}|%' อย่าลบของคนอื่น
  3. วนเอกสารของบริษัทนั้นเรียงตาม id จากเก่าไปใหม่ แล้วเรียก nextFor ใหม่ทีละใบ
  4. ถ้าบางบริษัทเคยออกเลขด้วยระบบเดิมมาก่อน ให้ seed ค่า last_no ต่อจากของเดิม ไม่ให้เลขชนกัน
  5. ลบไฟล์ PDF เก่าที่ชื่อไม่ตรงเลขใหม่ แล้วสั่งเรนเดอร์ใหม่ทั้งชุด

เรื่องสุดท้ายที่ทำผมเสียเวลาเปล่าไปครึ่งวัน

ตอนเทสต์ผมชอบยิง POST ด้วย curl เพราะเร็วดี ปรากฏว่าข้อมูลภาษาไทยที่ส่งผ่าน curl บน Git Bash ของ Windows มันเพี้ยนเป็นตัวขยะทุกครั้ง ผมนั่งไล่หาบั๊กในโค้ดจัดการ encoding อยู่นานมาก กว่าจะรู้ว่าโค้ดไม่ผิดเลย ตัวที่ผิดคือ shell ที่แปลง UTF-8 ตอนส่ง argument ไม่ครบ

สรุปสั้นๆ ว่า อย่าใช้ curl บน Git Bash ทดสอบข้อมูลภาษาไทย ให้ทดสอบผ่านเบราว์เซอร์จริง หรือสร้างข้อมูลทดสอบผ่าน tinker หรือ seeder แทน จะได้ไม่เสียเวลาไล่บั๊กที่ไม่มีอยู่จริง

ส่วนตัวผมสรุปงานนี้ได้ว่า การให้เบราว์เซอร์พิมพ์ PDF ให้เป็นทางที่คุ้มที่สุดสำหรับเอกสารไทยที่มี layout เยอะ แต่ต้องยอมรับว่าค่าใช้จ่ายมันไม่ได้อยู่ที่การเรนเดอร์ มันอยู่ที่การล็อกเวอร์ชันเบราว์เซอร์ การฝังฟอนต์ และการล้างข้อมูลที่ผู้ใช้พิมพ์เข้ามาก่อนเอาไปทำเป็นชื่อไฟล์ ใครจะเอาโครงนี้ไปใช้ ปรับ margin กับเพดานย่อตามแบบฟอร์มของตัวเองได้ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย