← Back to Works

The Deep Dive

A6 → A4 Label Printer

เครื่องมือจัดป้าย A6 ลงกระดาษ A4 แผ่นละ 4 ใบ ทำงานจากไฟล์ HTML ไฟล์เดียว ไม่ต้องติดตั้ง ไม่ต้องต่ออินเทอร์เน็ต

Year
2026
Category
Code
Tags
React, PDF, Print, Offline-first, Tooling
A6 → A4 Label Printer

Context

งานประจำมี label ขนาด A6 ที่ต้องพิมพ์เอง แต่เครื่องพิมพ์ที่มีเป็นเครื่องพิมพ์เอกสารทั่วไป ไม่ใช่เครื่องพิมพ์ label และเป็นเครื่องที่ใช้ร่วมกันข้ามแผนก อีกแผนกพิมพ์ A4 เป็นส่วนใหญ่ การสลับถาดกระดาษไปมาระหว่าง A6 กับ A4 จึงลำบาก ที่ผ่านมาต้องตัดกระดาษ A4 เป็น A6 เอง

จึงทำ web app นี้ขึ้นมา รวม label หลายงานลงกระดาษ A4 แผ่นเดียว พิมพ์ครั้งเดียวด้วยถาด A4 ตามปกติ แล้วตัดตามรอยประ รับ PDF หรือรูป จัดวางแบบ N-up ลงกระดาษขนาดใดก็ได้ พร้อมพรีวิวที่ตรงกับของจริงในหน่วยมิลลิเมตร ทั้งหมดอยู่ในไฟล์ HTML ไฟล์เดียว download แล้วดับเบิลคลิกเปิดได้เลย ไม่ต้องติดตั้ง ไม่ต้องต่ออินเทอร์เน็ต

Problem

ปัญหาไม่ได้อยู่ที่การจัดวาง แต่อยู่ที่ป้ายพิมพ์ออกมาผิดขนาด

สาเหตุอันดับหนึ่งคือกล่องพิมพ์ของ browser ตัวเลือก Fit to printable area ย่องานลงราว 96% โดยไม่บอก และถ้าระยะขอบไม่ได้ตั้งเป็น None มันจะซ้อนกับระยะขอบที่ตั้งไว้ในแอปอีกชั้น ป้ายจึงออกมาเล็กกว่าที่ควรและเลื่อนตำแหน่งไปไม่กี่มิลลิเมตร — น้อยพอที่จะไม่สังเกต แต่มากพอที่จะทำให้เส้นตัดคลาด

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

Role

ออกแบบและพัฒนาทั้งหมด

Process

เริ่มจากไล่หาว่าป้ายผิดขนาดเกิดจากอะไร แล้วพบว่าบ่อยที่สุดเกิดในกล่องพิมพ์ ไม่ใช่ในแอป ทิศทางจึงเปลี่ยนจาก “จัดวางให้สวย” เป็น “ทำให้สิ่งที่เห็นบนจอตรงกับสิ่งที่ออกจากเครื่องพิมพ์” ทุกอย่างหลังจากนั้นวัดด้วยเกณฑ์เดียว คือขนาดบนกระดาษถูกหรือไม่

งานจริงไม่ได้มาทีละไฟล์ — PDF หลายไฟล์ปนกับรูปอีกจำนวนหนึ่ง และแต่ละใบต้องการจำนวนสำเนาไม่เท่ากัน จึงออกแบบให้ทุกหน้าไหลลงคิวเดียว แล้วจัด N-up ต่อเนื่องข้ามไฟล์ ในคิวลากสลับลำดับได้ หมุน 90 องศารายหน้า ลบรายหน้าหรือรายไฟล์ กำหนดสำเนาต่อป้าย เลือกหน้าด้วย 1,3-5 หรือ 4- และเติมช่องว่างแผ่นสุดท้ายให้เอง

ขนาดไม่ได้ล็อกไว้ที่ A6 ลง A4 รองรับ A4 A5 A3 Letter Legal และกำหนดเอง ป้ายระบุเป็นมิลลิเมตร คอลัมน์คูณแถวถึง 12×12 มีปุ่มคำนวณให้ เพราะคณิตศาสตร์ของ N-up ไม่สนว่ากระดาษขนาดไหน ออกแบบให้รองรับทั่วไปตั้งแต่ต้นถูกกว่ามาแก้ทีหลัง

ระหว่างพัฒนาแยกซอร์สออกจากไฟล์ที่ใช้จริง src/index.src.html โหลดไลบรารีจาก CDN และคอมไพล์ JSX ตอนรัน เพื่อให้แก้แล้วรีเฟรชดูได้ทันที เมื่อ build จึงค่อยฝังทุกอย่างลง dist/index.html ไฟล์เดียวที่ทำงาน offline

Decisions

พรีวิวใช้หน่วยมิลลิเมตรจริง ไม่ใช่พิกเซล เพราะสิ่งที่ผู้ใช้ต้องเชื่อคือขนาดบนกระดาษ ถ้าบนจอบอกว่า 105 มิลลิเมตร บนกระดาษก็ต้องเป็น 105

แต่เครื่องพิมพ์แต่ละตัวไม่เท่ากัน จึงมีหน้าสอบเทียบ พิมพ์กรอบ 100×100 มิลลิเมตรออกมาให้วัดด้วยไม้บรรทัด แล้วกรอกค่าที่วัดได้กลับเข้าไป แอปใช้ค่านั้นชดเชย scale และ offset ของเครื่องพิมพ์ตัวนั้นโดยเฉพาะ แก้ที่ต้นเหตุครั้งเดียว ไม่ต้องเดาซ้ำทุกงาน

การตั้งค่าในกล่องพิมพ์เป็นสิ่งที่แอปควบคุมไม่ได้ ทำได้แค่เตือน จึงมีเช็คลิสต์ก่อนพิมพ์ทุกครั้ง Scale ต้องเป็น 100% ไม่ใช่ Fit to printable area และ Margins ต้องเป็น None เตือนซ้ำเพราะเป็นสาเหตุอันดับหนึ่ง

รูปที่ดูดีบนจออาจไม่พอสำหรับกระดาษ จึงเตือนเมื่อหน้าไหนต่ำกว่า 200 DPI ที่ขนาดป้ายจริง หรือสัดส่วนไม่ตรงกับกรอบ ให้รู้ก่อนพิมพ์ ไม่ใช่หลังพิมพ์

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

เส้นตัดมีสองแบบเพราะเครื่องมือตัดต่างกัน เส้นประสำหรับตัดมือ crop mark ที่มุมสำหรับเครื่องตัด และชดเชยขอบที่พิมพ์ไม่ได้ของเครื่องพิมพ์ได้แยกต่อป้าย

Technical Decisions

ทุกอย่างฝังอยู่ในไฟล์เดียว React, pdf.js, jsPDF และฟอนต์ไทย ไม่มีการเรียกเน็ตแม้แต่ครั้งเดียว เพราะ offline ที่มีข้อยกเว้นคือไม่ offline

พิมพ์ผ่าน hidden iframe แทนการเปิดหน้าต่างใหม่ เพราะ popup blocker จะกินหน้าต่างพิมพ์ไปเงียบ ๆ และผู้ใช้จะเข้าใจว่าแอปพัง

ทางออกมีสองทาง พิมพ์ตรงจาก browser หรือ Export PDF ขนาด A4 จริงด้วย jsPDF ต่างกันที่ขนาดไฟล์ เอกสาร HTML ฝังรูปซ้ำทุกป้าย ส่วน PDF ใช้รูปเดียวอ้างซ้ำได้ งานใหญ่มากแอปจึงเตือนและแนะนำให้ Export PDF แทน

งานถูกจำไว้ใน IndexedDB ปิด browser แล้วกลับมาทำต่อได้ และเมื่อพื้นที่ไม่พอแอปบอกตรง ๆ แทนที่จะเงียบแล้วปล่อยให้งานหาย

ลายน้ำข้อความรองรับภาษาไทยทั้งตอนพิมพ์และตอน Export PDF โดยฝังฟอนต์ Sarabun ให้เองเมื่อพบอักษรไทย

หลักการของการสอบเทียบย่อได้เท่านี้ วัดว่าเครื่องพิมพ์ย่อหรือขยายไปเท่าไร แล้วคูณกลับ ตัวอย่างย่อ:

// ค่าที่วัดได้จากกรอบสอบเทียบ 100×100 mm
type Calibration = {
  measuredWidth: number; // mm
  measuredHeight: number; // mm
  offsetX: number; // mm
  offsetY: number; // mm
};

export function compensate(point: { x: number; y: number }, cal: Calibration) {
  const scaleX = 100 / cal.measuredWidth;
  const scaleY = 100 / cal.measuredHeight;

  return {
    x: point.x * scaleX - cal.offsetX,
    y: point.y * scaleY - cal.offsetY,
  };
}

Result

เครื่องมือใช้งานได้จากไฟล์เดียว และความถูกต้องของมันพิสูจน์ด้วยเทสต์ ไม่ใช่ด้วยการลองพิมพ์ดู

เทสต์หน่วย 44 ข้อครอบคลุมเรขาคณิต N-up เส้นตัด crop mark คิวพิมพ์ การสอบเทียบ และ guard ขนาดไฟล์

e2e อีก 21 ข้อเปิด dist/index.html จาก file:// ใน Chromium จริง โดยบล็อกทุก request ที่เป็น http/https ถ้าแอปยังทำงานครบ แปลว่า offline จริง เทสต์ไปถึงขั้น download PDF ออกมาอ่าน /MediaBox ว่าเป็น A4 และวัดกรอบป้ายในเอกสารพิมพ์ได้กว้าง 101 มิลลิเมตร สูง 144 มิลลิเมตร ตรงกับค่าที่คาดไว้

Reflection

ปัญหาที่ใหญ่ที่สุดของเครื่องมือพิมพ์ไม่ได้อยู่ในเครื่องมือ แต่อยู่ในกล่องพิมพ์ที่เครื่องมือควบคุมไม่ได้ สิ่งที่ทำได้คือเตือนให้ถูกจังหวะ และให้วิธีวัดกลับเมื่อผลไม่ตรง

Offline เป็นสิ่งที่พิสูจน์ได้ ไม่ใช่คำอ้าง เทสต์ที่บล็อกทุก request แล้วยืนยันว่าแอปยังทำงานครบคือหลักฐานนั้น

ไฟล์เดียวมีราคา หน้าที่เปิดจาก file:// สร้าง Web Worker ไม่ได้ pdf.js จึงต้องทำงานบน main thread ไฟล์ใหญ่ render ช้ากว่าเปิดผ่าน web server และ jsPDF ไม่ทำ text shaping สระกับวรรณยุกต์ไทยในลายน้ำของ PDF จึงอาจวางตำแหน่งไม่สวยเท่าการพิมพ์ผ่าน browser ทั้งสองข้อเป็นราคาที่ยอมจ่าย เพื่อให้เปิดได้ทุกเครื่องโดยไม่ต้องติดตั้ง

Context

งานประจำมี label ขนาด A6 ที่ต้องพิมพ์เอง แต่เครื่องพิมพ์ที่มีเป็นเครื่องพิมพ์เอกสารทั่วไป ไม่ใช่เครื่องพิมพ์ label และเป็นเครื่องที่ใช้ร่วมกันข้ามแผนก อีกแผนกพิมพ์ A4 เป็นส่วนใหญ่ การสลับถาดกระดาษไปมาระหว่าง A6 กับ A4 จึงลำบาก ที่ผ่านมาต้องตัดกระดาษ A4 เป็น A6 เอง

จึงทำ web app นี้ขึ้นมา รวม label หลายงานลงกระดาษ A4 แผ่นเดียว พิมพ์ครั้งเดียวด้วยถาด A4 ตามปกติ แล้วตัดตามรอยประ รับ PDF หรือรูป จัดวางแบบ N-up ลงกระดาษขนาดใดก็ได้ พร้อมพรีวิวที่ตรงกับของจริงในหน่วยมิลลิเมตร ทั้งหมดอยู่ในไฟล์ HTML ไฟล์เดียว download แล้วดับเบิลคลิกเปิดได้เลย ไม่ต้องติดตั้ง ไม่ต้องต่ออินเทอร์เน็ต

Problem

ปัญหาไม่ได้อยู่ที่การจัดวาง แต่อยู่ที่ป้ายพิมพ์ออกมาผิดขนาด

สาเหตุอันดับหนึ่งคือกล่องพิมพ์ของ browser ตัวเลือก Fit to printable area ย่องานลงราว 96% โดยไม่บอก และถ้าระยะขอบไม่ได้ตั้งเป็น None มันจะซ้อนกับระยะขอบที่ตั้งไว้ในแอปอีกชั้น ป้ายจึงออกมาเล็กกว่าที่ควรและเลื่อนตำแหน่งไปไม่กี่มิลลิเมตร — น้อยพอที่จะไม่สังเกต แต่มากพอที่จะทำให้เส้นตัดคลาด

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

Role

ออกแบบและพัฒนาทั้งหมด

Process

เริ่มจากไล่หาว่าป้ายผิดขนาดเกิดจากอะไร แล้วพบว่าบ่อยที่สุดเกิดในกล่องพิมพ์ ไม่ใช่ในแอป ทิศทางจึงเปลี่ยนจาก “จัดวางให้สวย” เป็น “ทำให้สิ่งที่เห็นบนจอตรงกับสิ่งที่ออกจากเครื่องพิมพ์” ทุกอย่างหลังจากนั้นวัดด้วยเกณฑ์เดียว คือขนาดบนกระดาษถูกหรือไม่

งานจริงไม่ได้มาทีละไฟล์ — PDF หลายไฟล์ปนกับรูปอีกจำนวนหนึ่ง และแต่ละใบต้องการจำนวนสำเนาไม่เท่ากัน จึงออกแบบให้ทุกหน้าไหลลงคิวเดียว แล้วจัด N-up ต่อเนื่องข้ามไฟล์ ในคิวลากสลับลำดับได้ หมุน 90 องศารายหน้า ลบรายหน้าหรือรายไฟล์ กำหนดสำเนาต่อป้าย เลือกหน้าด้วย 1,3-5 หรือ 4- และเติมช่องว่างแผ่นสุดท้ายให้เอง

ขนาดไม่ได้ล็อกไว้ที่ A6 ลง A4 รองรับ A4 A5 A3 Letter Legal และกำหนดเอง ป้ายระบุเป็นมิลลิเมตร คอลัมน์คูณแถวถึง 12×12 มีปุ่มคำนวณให้ เพราะคณิตศาสตร์ของ N-up ไม่สนว่ากระดาษขนาดไหน ออกแบบให้รองรับทั่วไปตั้งแต่ต้นถูกกว่ามาแก้ทีหลัง

ระหว่างพัฒนาแยกซอร์สออกจากไฟล์ที่ใช้จริง src/index.src.html โหลดไลบรารีจาก CDN และคอมไพล์ JSX ตอนรัน เพื่อให้แก้แล้วรีเฟรชดูได้ทันที เมื่อ build จึงค่อยฝังทุกอย่างลง dist/index.html ไฟล์เดียวที่ทำงาน offline

Decisions

พรีวิวใช้หน่วยมิลลิเมตรจริง ไม่ใช่พิกเซล เพราะสิ่งที่ผู้ใช้ต้องเชื่อคือขนาดบนกระดาษ ถ้าบนจอบอกว่า 105 มิลลิเมตร บนกระดาษก็ต้องเป็น 105

แต่เครื่องพิมพ์แต่ละตัวไม่เท่ากัน จึงมีหน้าสอบเทียบ พิมพ์กรอบ 100×100 มิลลิเมตรออกมาให้วัดด้วยไม้บรรทัด แล้วกรอกค่าที่วัดได้กลับเข้าไป แอปใช้ค่านั้นชดเชย scale และ offset ของเครื่องพิมพ์ตัวนั้นโดยเฉพาะ แก้ที่ต้นเหตุครั้งเดียว ไม่ต้องเดาซ้ำทุกงาน

การตั้งค่าในกล่องพิมพ์เป็นสิ่งที่แอปควบคุมไม่ได้ ทำได้แค่เตือน จึงมีเช็คลิสต์ก่อนพิมพ์ทุกครั้ง Scale ต้องเป็น 100% ไม่ใช่ Fit to printable area และ Margins ต้องเป็น None เตือนซ้ำเพราะเป็นสาเหตุอันดับหนึ่ง

รูปที่ดูดีบนจออาจไม่พอสำหรับกระดาษ จึงเตือนเมื่อหน้าไหนต่ำกว่า 200 DPI ที่ขนาดป้ายจริง หรือสัดส่วนไม่ตรงกับกรอบ ให้รู้ก่อนพิมพ์ ไม่ใช่หลังพิมพ์

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

เส้นตัดมีสองแบบเพราะเครื่องมือตัดต่างกัน เส้นประสำหรับตัดมือ crop mark ที่มุมสำหรับเครื่องตัด และชดเชยขอบที่พิมพ์ไม่ได้ของเครื่องพิมพ์ได้แยกต่อป้าย

Technical Decisions

ทุกอย่างฝังอยู่ในไฟล์เดียว React, pdf.js, jsPDF และฟอนต์ไทย ไม่มีการเรียกเน็ตแม้แต่ครั้งเดียว เพราะ offline ที่มีข้อยกเว้นคือไม่ offline

พิมพ์ผ่าน hidden iframe แทนการเปิดหน้าต่างใหม่ เพราะ popup blocker จะกินหน้าต่างพิมพ์ไปเงียบ ๆ และผู้ใช้จะเข้าใจว่าแอปพัง

ทางออกมีสองทาง พิมพ์ตรงจาก browser หรือ Export PDF ขนาด A4 จริงด้วย jsPDF ต่างกันที่ขนาดไฟล์ เอกสาร HTML ฝังรูปซ้ำทุกป้าย ส่วน PDF ใช้รูปเดียวอ้างซ้ำได้ งานใหญ่มากแอปจึงเตือนและแนะนำให้ Export PDF แทน

งานถูกจำไว้ใน IndexedDB ปิด browser แล้วกลับมาทำต่อได้ และเมื่อพื้นที่ไม่พอแอปบอกตรง ๆ แทนที่จะเงียบแล้วปล่อยให้งานหาย

ลายน้ำข้อความรองรับภาษาไทยทั้งตอนพิมพ์และตอน Export PDF โดยฝังฟอนต์ Sarabun ให้เองเมื่อพบอักษรไทย

หลักการของการสอบเทียบย่อได้เท่านี้ วัดว่าเครื่องพิมพ์ย่อหรือขยายไปเท่าไร แล้วคูณกลับ ตัวอย่างย่อ:

// ค่าที่วัดได้จากกรอบสอบเทียบ 100×100 mm
type Calibration = {
  measuredWidth: number; // mm
  measuredHeight: number; // mm
  offsetX: number; // mm
  offsetY: number; // mm
};

export function compensate(point: { x: number; y: number }, cal: Calibration) {
  const scaleX = 100 / cal.measuredWidth;
  const scaleY = 100 / cal.measuredHeight;

  return {
    x: point.x * scaleX - cal.offsetX,
    y: point.y * scaleY - cal.offsetY,
  };
}

Result

เครื่องมือใช้งานได้จากไฟล์เดียว และความถูกต้องของมันพิสูจน์ด้วยเทสต์ ไม่ใช่ด้วยการลองพิมพ์ดู

เทสต์หน่วย 44 ข้อครอบคลุมเรขาคณิต N-up เส้นตัด crop mark คิวพิมพ์ การสอบเทียบ และ guard ขนาดไฟล์

e2e อีก 21 ข้อเปิด dist/index.html จาก file:// ใน Chromium จริง โดยบล็อกทุก request ที่เป็น http/https ถ้าแอปยังทำงานครบ แปลว่า offline จริง เทสต์ไปถึงขั้น download PDF ออกมาอ่าน /MediaBox ว่าเป็น A4 และวัดกรอบป้ายในเอกสารพิมพ์ได้กว้าง 101 มิลลิเมตร สูง 144 มิลลิเมตร ตรงกับค่าที่คาดไว้

Reflection

ปัญหาที่ใหญ่ที่สุดของเครื่องมือพิมพ์ไม่ได้อยู่ในเครื่องมือ แต่อยู่ในกล่องพิมพ์ที่เครื่องมือควบคุมไม่ได้ สิ่งที่ทำได้คือเตือนให้ถูกจังหวะ และให้วิธีวัดกลับเมื่อผลไม่ตรง

Offline เป็นสิ่งที่พิสูจน์ได้ ไม่ใช่คำอ้าง เทสต์ที่บล็อกทุก request แล้วยืนยันว่าแอปยังทำงานครบคือหลักฐานนั้น

ไฟล์เดียวมีราคา หน้าที่เปิดจาก file:// สร้าง Web Worker ไม่ได้ pdf.js จึงต้องทำงานบน main thread ไฟล์ใหญ่ render ช้ากว่าเปิดผ่าน web server และ jsPDF ไม่ทำ text shaping สระกับวรรณยุกต์ไทยในลายน้ำของ PDF จึงอาจวางตำแหน่งไม่สวยเท่าการพิมพ์ผ่าน browser ทั้งสองข้อเป็นราคาที่ยอมจ่าย เพื่อให้เปิดได้ทุกเครื่องโดยไม่ต้องติดตั้ง