การแปลง HTML เป็น PDF ดูเหมือนจะเป็นเรื่องง่ายในทางทฤษฎี คุณสร้างเทมเพลตที่สวยงาม ใส่ข้อมูลลงไป และคาดหวังว่าจะได้เอกสารที่เหมือนกับหน้าเว็บแบบพิกเซลต่อพิกเซล แต่ในความเป็นจริง กระบวนการทำงาน (pipeline) มักจะกลายเป็นการต่อสู้รายวันกับปัญหาโปรแกรมค้าง (crashes), ตัวอักษรหาย (missing glyphs) และความผิดเพี้ยนของภาพ (visual corruption) ในโปรเจกต์ล่าสุดที่ผ่านมา มีปัญหา 3 อย่างที่เกิดขึ้นซ้ำๆ คือ: iText จะล่มไปเลยเมื่อเจอไฟล์กราฟิก SVG บางประเภท, อีโมจิหายไปกลายเป็นช่องสี่เหลี่ยมสีขาวว่างเปล่า, และพื้นหลังโปร่งใสที่ดูละเอียดอ่อนกลับกลายเป็นบล็อกสีดำทึบ ความล้มเหลวแต่ละอย่างมีสาเหตุที่แตกต่างกัน และการแก้ไขทั้งสามปัญหานี้จำเป็นต้องคิดทบทวนใหม่ว่าแอปพลิเคชันควรเตรียมเนื้อหาอย่างไรก่อนที่เอนจิน PDF จะเริ่มทำงาน

เมื่อ SVG ทำให้ Pipeline พัง

iText มาพร้อมกับ SVG renderer ภายในเพื่อความสะดวก แต่การรวมกันนี้กลับซ่อนจุดอ่อนที่สำคัญไว้ เมื่อ SVG มีเส้นทาง (paths) ที่ซับซ้อน, การจัดสไตล์ด้วย CSS ที่หนักหน่วง หรือการแปลงพิกัด (coordinate transformations) บางอย่าง ตัว parser ที่ฝังอยู่จะไม่แจ้งข้อผิดพลาด (exception) อย่างเป็นระเบียบแล้วทำงานต่อ แต่มันจะ "ระเบิด" ออกมาเลย นี่คือการค้างของระบบทั้งหมดที่ทำให้ thread การสร้าง PDF ตายลงโดยไม่มีการแจ้งเตือน ทิ้งไว้เพียงไฟล์ที่ไม่สมบูรณ์และ stack trace ที่ชี้ไปยังส่วนลึกภายใน vector parser

วิธีแก้ไขที่เชื่อถือได้คือการเลิกใช้ iText ในการ render SVG ไปเลย แต่ให้ย้ายงานนั้นไปใช้ Apache Batik ที่รันในโหมด standalone แทน Batik สามารถจัดการกับเส้นทางที่ซับซ้อนและกฎ CSS แบบเดียวกันได้โดยไม่เปราะบางเท่า และการแยกส่วนกันจะช่วยปกป้อง PDF engine ของคุณจากความไม่เสถียรที่เกี่ยวข้องกับกราฟิก ขั้นตอนการทำงานนั้นตรงไปตรงมา: ก่อนเริ่มการประกอบเอกสาร ให้รัน SVG ผ่าน Batik เพื่อสร้าง PNG data URL จากนั้นส่งรูปภาพแบบ raster นั้นเข้าไปใน iText แทนที่จะใช้ markup แบบ vector ดิบๆ Batik แบบ standalone ติดตามข้อกำหนด (specification) ของ SVG ได้ใกล้ชิดกว่า renderer ที่ฝังและถูกล็อกไว้ภายในไลบรารีขนาดใหญ่ และการแยกส่วนนี้หมายความว่ากราฟิกที่ผิดรูปแบบจะไม่สามารถทำให้การแปลงเอกสารทั้งหมดล่มลงได้

รายละเอียดเล็กๆ เพียงอย่างเดียวจะเป็นตัวตัดสินว่าแผนภูมิของคุณจะดูเป็นมืออาชีพหรือดูเหมือนรายงานข้อผิดพลาด (bug report) โดย SVG จะพึ่งพา attribute viewBox ในการกำหนดระบบพิกัดและพฤติกรรมการปรับขนาด หากโค้ดการแปลงของคุณละเลย viewBox แผนภูมิที่ถูกต้องสมบูรณ์อาจหดตัวลงจนเหลือเพียงจุดเล็กๆ ที่อ่านไม่ออก หรือยืดออกจนบิดเบี้ยว จง parse attribute นี้อย่างชัดเจนและแมปขนาดเหล่านั้นเข้ากับขนาดผลลัพธ์ของคุณ การข้ามขั้นตอนนี้จะทำให้คุณต้องเสียเวลาหลายชั่วโมงในการแก้ปัญหา layout ซึ่งไม่เกี่ยวกับคุณภาพการ render เลย แต่เกี่ยวข้องกับการประกาศพิกัดที่ขาดหายไปทั้งหมด

ปัญหาหมึกล่องหน

ช่องสี่เหลี่ยมว่างเปล่าในจุดที่ควรจะเป็นอีโมจิบอกเล่าเรื่องราวที่เรียบง่าย นั่นคือ ฟอนต์ปัจจุบันไม่รองรับภาษานั้น Helvetica และฟอนต์ PDF มาตรฐานอื่นๆ ถูกสร้างขึ้นก่อนที่จะมีการใช้อีโมจิอย่างแพร่หลาย พวกมันจึงไม่มี glyph สำหรับช่วง Unicode ของอีโมจิ ดังนั้นเมื่อ iText เจอ code points เหล่านั้น มันจึงไม่ render อะไรเลยและข้ามไป ผลลัพธ์คือเอกสารที่เต็มไปด้วยกล่องว่างเปล่า ซึ่งทำให้รายงานความรู้สึกทางสังคม (social sentiment reports) หรือการส่งออกความคิดเห็นของผู้ใช้ดูเหมือนพัง

คุณไม่สามารถพึ่งพา OS ของเครื่องผู้ใช้เพื่อเติมเต็มช่องว่างนี้ได้ เพราะ PDF มีทรัพยากรฟอนต์ของตัวเอง และสิ่งที่ดูถูกต้องในเบราว์เซอร์จะไม่มีความหมายเลยเมื่อไฟล์ถูกแยกออกจากฟอนต์ในระบบของคุณ วิธีแก้ไขคือการสร้างเลเยอร์การกำหนดเส้นทางฟอนต์ (font routing layer) ที่ชัดเจน โดยการลงทะเบียนฟอนต์ที่รองรับอีโมจิโดยเฉพาะ เช่น Symbola ซึ่งมีสัญลักษณ์ขาวดำที่ครอบคลุมบล็อก Unicode ของอีโมจิ รูปหัวใจหรือสัญลักษณ์เตือนแบบขาวดำอาจจะดูไม่สวยงามเท่ากับชุด glyph สีสันสดใส แต่มันสื่อความหมายได้ ในขณะที่สี่เหลี่ยมว่างเปล่าสื่อถึงความล้มเหลว ฟอนต์อีโมจิแบบเต็มสี (full-color) ยังคงเป็นเรื่องยากที่จะ render ให้สม่ำเสมอภายใน PDF viewer และการพยายามรองรับสีมักจะสร้างปัญหาความเข้ากันได้ (compatibility) มากกว่าที่จะแก้ไขได้

iText ยังเพิ่มปัญหาที่สองที่แย่กว่าเดิมผ่านการตัดบรรทัด (line breaking) โดยไลบรารีอาจแยก emoji surrogate pairs ตรงจุดรอยต่อที่ผิดพลาด ทำให้ตัวอักษรตัวเดียวถูกฉีกออกเป็นสองส่วนที่ไม่ถูกต้อง เมื่อสิ่งนั้นเกิดขึ้น สตรีมข้อความจะเสียหาย และคุณจะจบลงด้วยเศษซากข้อความที่อ่านไม่ออกในจุดที่ควรจะเป็น glyph ตัวเดียว เพื่อป้องกันสิ่งนี้ ให้ใช้ ISplitCharacter แบบกำหนดเองที่รู้จัก surrogate pairs และปฏิบัติกับพวกมันในฐานะหน่วยอะตอม (atomic units) วิธีนี้จะช่วยหยุดเอนจิน layout ไม่ให้แทรกการตัดบรรทัดกลางอีโมจิ และรักษาความสมบูรณ์ของข้อความไว้

เมื่อความโปร่งใสกลายเป็นสีดำ

SVG ที่มีพื้นหลัง rgba แบบนุ่มนวลหรือเอฟเฟกต์ fill-opacity แบบเป็นเลเยอร์จะดูสวยงามในเบราว์เซอร์ แต่เมื่อส่ง markup แบบเดียวกันนั้นเข้าไปใน iText ความโปร่งใสก็มักจะกลายเป็นสี่เหลี่ยมสีดำทึบ เนื่องจากเอนจินจัดการฟังก์ชันสี CSS และ attribute opacity ผิดพลาด โดยแทนที่ความโปร่งใสด้วยหมึกที่มีความเข้มสูงสุด

การทำ Pre-processing ให้กับ SVG ก่อนที่จะส่งไปยังตัวแปลง (converter) คือการป้องกันเพียงอย่างเดียวที่เชื่อถือได้ ให้ลบหรือแทนที่องค์ประกอบใดๆ ที่ต้องพึ่งพา alpha blending เปลี่ยนค่า rgba() ให้เป็นสี rgb() แบบทึบ หากคุณจำเป็นต้องรักษาความโปร่งใส (opacity) ไว้บ้าง ให้ย้ายค่าออกจาก CSS shorthand ไปยัง attribute opacity มาตรฐาน แม้ว่าการลบความโปร่งใสออกไปทั้งหมดจะเป็นวิธีที่ปลอดภัยที่สุดก็ตาม การเปลี่ยนแปลงเหล่านี้อาจดูเหมือนเป็นการถอยหลังเข้าคลองสำหรับการออกแบบเว็บ แต่ PDF ใช้โมเดลการสร้างภาพ (imaging model) ที่แตกต่างออกไปและมีมาก่อนความโปร่งใสของ CSS สมัยใหม่ รูปแบบนี้ต้องการค่าสีที่ชัดเจน และการให้ค่าที่คลุมเครือจะนำไปสู่ความผิดพลาดที่ร้ายแรง

ในขณะที่คุณกำลังทำความสะอาด markup (sanitizing) ให้ตรวจสอบอีกครั้งว่า SVG ทุกตัวมีการประกาศ namespace xmlns ที่ถูกต้อง HTML ที่ถูกสร้างขึ้นและ template engines มักจะตัด attribute ของ namespace ออกระหว่างกระบวนการ minification หรือ DOM serialization หากไม่มี namespace นั้น SVG parser อาจระบุองค์ประกอบผิดพลาดหรือทำงานล้มเหลวโดยไม่แจ้งเตือน ซึ่งจะทำให้เกิด parser error หรือข้อมูล vector ที่ผิดรูปแบบจนไม่สามารถแสดงผลบนหน้าเว็บได้ มันเป็นการตรวจสอบพื้นฐานที่ใช้เวลาเพียงไม่กี่วินาทีแต่ช่วยประหยัดเวลาได้หลายชั่วโมง

เทมเพลตเดียว สองโลก

วิธีแก้ปัญหาในระยะยาวที่แย่ที่สุดคือการรักษา HTML templates แยกกันระหว่างเบราว์เซอร์และ PDF ป้ายกำกับ (labels) จะเริ่มคลาดเคลื่อน ระยะขอบ (margins) จะเปลี่ยนไป และในไม่ช้ารายงานที่ส่งออกก็จะไม่มีความสอดคล้องกับแดชบอร์ดอีกต่อไป สถาปัตยกรรมที่สะอาดกว่านั้นจะอาศัยเทมเพลตเพียงชุดเดียว และแยกตรรกะการแสดงผล (rendering logic) ด้วย flag เพียงตัวเดียว เช่น context.isForPdf()

เมื่อ flag นั้นเป็น false เทมเพลตจะมอบประสบการณ์การใช้งานบนเบราว์เซอร์แบบเต็มรูปแบบ มันจะแสดงผล native SVG เพื่อการซูมแบบไม่จำกัด, CSS สมัยใหม่ และ assets สีต่างๆ ที่เบราว์เซอร์รองรับ เมื่อ flag เป็น true เทมเพลตเดิมจะเปลี่ยน assets จาก SVG เป็น PNG ที่ถูก render ไว้ล่วงหน้า, เปิดใช้งาน font stack ที่รองรับ emoji และลบเอฟเฟกต์ความโปร่งใสที่ไม่รองรับออกไป ข้อความและโครงสร้างจะยังคงเดิม มีเพียง asset pipeline และกฎการจัดรูปแบบ (styling rules) เท่านั้นที่ปรับเปลี่ยนตามสื่อเป้าหมาย

แนวทางแบบสองเส้นทาง (dual-path) นี้ช่วยให้ codebase มีความถูกต้องแม่นยำ คุณอัปเดตเนื้อหาในที่เดียว และเลเยอร์การกำหนดเส้นทาง (routing layer) จะจัดการความแตกต่างทางเทคนิคระหว่างหน้าจอและกระดาษเอง นอกจากนี้ยังทำให้การทดสอบง่ายขึ้นด้วย คุณสามารถตรวจสอบตรรกะของเทมเพลตในเบราว์เซอร์ด้วย developer tools แบบเต็มรูปแบบ จากนั้นจึงเปิดใช้งาน flag สำหรับ PDF และยืนยันว่าข้อมูลชุดเดียวกันนั้นสามารถสร้างเอกสารที่สมบูรณ์ได้โดยไม่ทำให้ตัวแปลง (converter) ค้างหรือพัง

ความจริงอันโหดร้ายเกี่ยวกับการสร้าง PDF

PDF จะไม่มีวันทำงานเหมือนเบราว์เซอร์ โมเดลการแสดงผลนั้นแตกต่างกันโดยพื้นฐาน และไลบรารีอย่าง iText จะมีการแลกเปลี่ยน (trade-offs) ระหว่างความเร็ว ขนาดไฟล์ และการปฏิบัติตามข้อกำหนด (specification compliance) อย่างตั้งใจ ความสำเร็จไม่ได้มาจากการพยายามต่อสู้กับ engine และหวังว่าผลจะออกมาดีที่สุด แต่มันมาจากการยอมรับข้อจำกัดตั้งแต่เนิ่นๆ และออกแบบ pipeline รอบๆ ข้อจำกัดเหล่านั้น

แปลง vector ของคุณก่อนถึงขั้นตอน PDF กำหนดเส้นทางฟอนต์ของคุณอย่างชัดเจนเพื่อให้ทุก glyph มี fallback ลบความโปร่งใสออกให้เหลือเพียงสีทึบ ให้บริบท (context) แก่เทมเพลตของคุณเพื่อให้พวกมันรู้ว่ากำลังแสดงผลสำหรับโลกใบไหน ทำสิ่งนี้อย่างสม่ำเสมอ แล้วเอกสารของคุณจะเลิกต่อสู้กับ renderer และเริ่มแสดงผลได้ตรงตามที่คุณตั้งใจไว้ทุกประการ