บทความเทคนิค

การ Flatten XFA Rich-Text Hyperlinks เป็น PDF Links ใน Delphi

XFA หรือ XML Forms Architecture ถูกยกเลิกแล้ว ISO 32000-1 มีมันใน §12.7 พร้อมหมายเหตุว่ามันถูกลบออกจาก PDF 2.0 และ viewers สมัยใหม่กำลังยกเลิก XFA engines ทีละตัว ไม่มีสิ่งใดทำให้ archives ว่างเปล่า แบบฟอร์มรับเข้าของรัฐบาล, ใบสมัครประกัน และใบแจ้งยอดธนาคารถูก author เป็น XFA มาเกือบสองทศวรรษ และไฟล์เหล่านั้นยังคงมาถึง inboxes และ document pipelines ในปัจจุบัน เมื่อ viewer ที่เคย render มันหยุดทำเช่นนั้น แบบฟอร์มจะกลายเป็นหน้าว่างพร้อม placeholder "โปรดเปิดใน reader อื่น" การแก้ไขที่ทนทานคือการ flatten XFA เป็น static PDF content ที่ reader ใดก็สามารถ paint ได้

ส่วนที่ยากของการ flattening นั้นไม่ใช่ fields text boxes และ check boxes จับคู่กับ AcroForm widgets ได้สะอาดพอ ส่วนที่ยากคือ rich text ที่ XFA เก็บไว้ภายใน draw element ใน <exData contentType="text/html"> block บล็อกนั้นคือ HTML subset ที่มี inline styling และมักมี anchors การนำมันไว้บนหน้ากระดาษหมายถึงการทำซ้ำทั้ง styled text และ live hyperlinks และ hyperlinks คือจุดที่ implementations ส่วนใหญ่ยอมแพ้อย่างเงียบ ๆ

XFA rich text ดูเหมือนอะไรจริง ๆ

body ของ exData คือ XHTML ชิ้นเล็ก ๆ paragraph คือ <p> span ที่มี styled characters คือ <span> ที่มี inline CSS ของตัวเองสำหรับ weight, posture, color และ size และ hyperlink คือ <a href="..."> ที่ wrap ข้อความที่มองเห็นได้ บรรทัดเดียวสามารถมีหลาย spans ต่อกัน แต่ละอันมี styling ต่างกัน และหนึ่งในนั้นสามารถเป็น anchor ได้ styling ไม่ใช่การตกแต่งที่สามารถลดทอนได้ ข้อความใน bold red เพราะเป็นคำเตือนทางกฎหมายต้องยังคง bold และ red หลังจาก flattening มิเช่นนั้นเอกสารที่ flatten แล้วจะแสดงเนื้อหาต้นฉบับไม่ถูกต้อง

ดังนั้น flatten engine ไม่สามารถปฏิบัติต่อ block เป็น string เดียว มันต้องเดินโครงสร้าง inline แก้ไข effective style ของแต่ละ run โดยการซ้อน inline CSS ของ span ทับ base font ของ draw element และวาง runs ต่อกันตลอดทั้งบรรทัด HotPDF จำลอง fragments ที่วางไว้เหล่านี้เป็น TXFARichRun record ภายใน record นั้นมีข้อความของ run, resolved style, measured box และสำหรับ anchor คือ Href ที่มันชี้ไป

การวาง runs จากซ้ายไปขวา

การวางตำแหน่งคือจุดที่ rich text หยุดเป็นปัญหาการ parsing และกลายเป็นปัญหาการเรียงพิมพ์ runs ใช้บรรทัดร่วมกัน ดังนั้นแต่ละ run เริ่มต้นที่ที่ run ก่อนหน้าสิ้นสุด ไม่มี markup ที่บันทึกตำแหน่งเหล่านั้น ต้องวัด routine ภายใน LayoutRichText ของ engine วัด run ทุก run ด้วย font metrics เดียวกับที่จะ paint ในภายหลัง จากนั้นตั้งค่า horizontal offset ของ run เป็นผลรวมสะสมของความกว้าง run ก่อนหน้าทั้งหมด Run หนึ่งเริ่มที่ draw box origin run สองเริ่มที่ความกว้างของ run หนึ่ง run สามที่ความกว้างรวมของสองตัวแรก และต่อไปเรื่อย ๆ ตลอดทั้งบรรทัด

นี่คือเหตุผลที่ measurement font alignment มีความสำคัญมาก pass layout วัด advances, pass render แยกต่างหากวาด glyphs ถ้า two passes ไม่เห็นด้วยกันเกี่ยวกับ font, boxes ที่ layout คำนวณจะไม่อยู่ใต้ glyphs ที่ renderer วาด HotPDF รักษาให้มันสอดคล้องกันโดย mapping resolved style ของแต่ละ run กับ font specification ผ่าน RunStyleToFontSpec helper ภายใน ที่ตรงกับค่าเริ่มต้นของ renderer ซึ่งก็คือ Arial ที่ 10 points advance ที่วัดได้และข้อความที่วาดจึงตกลงกัน และ box ที่คำนวณของ run ครอบคลุมตัวอักษรที่ผู้อ่านเห็นจริง ๆ

แผนภาพ HotPDF แบนบล็อก rich text exData ของ XFA ใน Delphi เป็น run ที่มีสไตล์ วางเรียงซ้ายไปขวาด้วยความกว้างที่วัดได้ โดย run ของ anchor เก็บ href ของตัวเอง และ fragment แต่ละชิ้นกลายเป็น record TXFARichRun
เอนจิน flatten เดินโครงสร้าง exData แบบ inline แก้สไตล์ของแต่ละชุด และวัดความกว้างด้วยฟอนต์ที่ใช้เรนเดอร์ กล่องที่จัดเลย์เอาต์แล้วจึงนั่งอยู่ใต้กลิฟที่วาดพอดีเป๊ะ
// รูปร่างเชิงแนวคิดของ run ที่วางหนึ่งตัว เครื่องมือสร้าง array ของสิ่งเหล่านี้
// ภายใน; คุณไม่ต้องสร้างเอง แต่ฟิลด์เหล่านี้อธิบายว่า
// hit box ของ link ได้มาอย่างไรจากเรขาคณิตที่วัดได้ แทนที่จะมาจากข้อความ
type
  TRichRunInfo = record
    Dx, Dy : Double;       // บนซ้าย, สัมพันธ์กับจุดกำเนิดของ draw-box
    W, H   : Double;       // กล่อง run ที่วัดแล้ว (ความกว้างจาก layout pass)
    Text   : AnsiString;   // ตัวอักษรที่มองเห็นได้ของ run
    Href   : AnsiString;   // เป้าหมาย URI สำหรับ run แบบ <a>, '' หากไม่ใช่
  end;

จาก anchor run ไปยัง PDF Link annotation

hyperlink ใน PDF ที่เสร็จสมบูรณ์ไม่ใช่ส่วนหนึ่งของ page content มันคือ object แยกต่างหาก, Link annotation, ที่อธิบายไว้ใน ISO 32000-1 §12.5.6.5 annotation มี /Rect ที่กำหนด clickable rectangle บนหน้าและ action ที่ fires เมื่อ rectangle ถูกคลิก สำหรับ external link action คือ URI action: /S /URI พร้อม target address เป็น /URI string ข้อความที่มองเห็นข้างใต้คือ ordinary page content annotation คือ invisible hot zone ที่วางทับมัน

เส้นทาง flatten ตามแบบจำลองนี้พอดี เมื่อ run มี Href HotPDF ก่อนอื่นวาด styled text แล้วสร้าง Link annotation บน box ของ run จุดเข้าสาธารณะสำหรับ annotation นั้นคือ page method AddURILink ซึ่งสร้าง /Type /Annot /Subtype /Link object ที่มี /URI action และคืน annotation dictionary สี่เหลี่ยมของมันคือ measured box ของ run, แปลงจาก local coordinates ของ draw element เป็น page coordinates ผลลัพธ์คือ link ที่ตกอยู่บน anchor text อย่างแม่นยำและไม่ที่อื่น

แผนภาพเส้นทาง flatten ของ HotPDF ที่แปลงกล่อง draw-local ที่วัดได้ของ run ของ anchor เข้าสู่พิกัดหน้า และปล่อย annotation Subtype Link พร้อม action URI ที่ Rect กอดข้อความ anchor พอดี
ชุดข้อความแองเคอร์กลายเป็นข้อความที่วาดแล้วบวกกับ annotation แบบ Link ซึ่ง /Rect คือกล่องที่วัดไว้ของชุดนั้นแปลงเข้าพิกัดหน้ากระดาษ โดยมี action URI ถูกสร้างด้วย AddURILink
// พื้นที่ประชิดกับ API สาธารณะเดียวกับที่ flatten path ใช้สำหรับแต่ละ anchor run มันสร้าง
// Link annotation ตามมาตรฐาน ISO 32000-1 12.5.6.5: /Subtype /Link พร้อม /URI action
// ทับสี่เหลี่ยมที่กำหนด คำอธิบายเสริมจะเติม /Contents เพื่อให้
// โปรแกรมอ่านจอภาพสามารถประกาศเป้าหมายได้
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // hit box ในหน้า-space สำหรับ run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

เหตุใด hit box จึงต้องมาจากความกว้างที่วัดได้

เป็นเรื่องน่าล่อใจที่จะจินตนาการถึงการค้นหา link โดยการค้นหา visible text บนหน้าและวาง rectangle รอบสิ่งที่พบ แต่มันไม่ได้ผล และเหตุผลนั้นเป็นพื้นฐานสำหรับวิธีการเก็บข้อความที่ flatten แล้ว styled runs ถูก paint ด้วย embedded subset fonts subset font จะนับหมาย glyphs ที่มันเก็บไว้ใหม่ ดังนั้น page content stream มี hexadecimal CID codes ไม่ใช่ character codes ดั้งเดิม bytes บนหน้าไม่ใช่ตัวอักษรที่มนุษย์อ่านและไม่สามารถค้นหาเป็นข้อความได้ การค้นหา caption ของ anchor ไม่พบอะไร เพราะ caption นั้นไม่มีอยู่เป็น literal text ที่ใดในหน้า stream

สมอที่เชื่อถือได้สำหรับ rectangle มีเพียงเรขาคณิตที่ layout pass สร้างขึ้นแล้ว offset และความกว้างที่วัดของแต่ละ run ถูกคำนวณขณะ flow บรรทัดก่อนที่ glyph ใดจะถูกนับใหม่ และอธิบายตำแหน่งที่ข้อความจะปรากฏทางกายภาพ HotPDF จึงนำ link rectangle จาก laid-down box ของ run โดยตรงแทนที่จะค้นหาข้อความใดๆ เพราะการวัดใช้ render font box จึงถูกต้องโดยไม่คำนึงถึงการ subsetting เรขาคณิตรอดจากการเข้ารหัส ข้อความไม่รอด นั่นคือข้อโต้แย้งทั้งหมดสำหรับ measured-width positioning และนั่นคือเหตุผลที่ flattener ที่พยายาม retrofit links โดยการค้นหาข้อความสร้าง hit zones ที่ drift หรือหายไป

แผนภาพแสดงเหตุผลที่ HotPDF ใช้กล่อง hit ของลิงก์ XFA จากเรขาคณิตของ run ที่วัดไว้: ฟอนต์ย่อยรันเลข glyph ใหม่เป็นรหัส CID ทำให้การค้นหาข้อความหาอะไรไม่เจอ ขณะที่ออฟเซ็ตและความกว้างจากรอบ layout ยังอยู่และให้ Rect ที่ถูกต้อง
ฟอนต์ซับเซ็ตที่ฝังไว้รันเลขกลิฟใหม่เป็นโค้ด CID การค้นหาข้อความจึงไม่เจออะไรเลย ออฟเซ็ตและความกว้างที่ขั้นตอนเลย์เอาต์วัดไว้เป็นแองเคอร์ชุดเดียวที่รอดจากการเข้ารหัส

การขับเคลื่อน flatten จาก code ของคุณ

สำหรับ PDF ที่มี XFA packet อยู่แล้ว จุดเข้าคือ FlattenLoadedXFA โหลดเอกสาร เรียก method และบันทึกผลลัพธ์ parameter Editable ตัดสินว่าจะเกิดอะไรกับ form fields: ส่ง True เพื่อเก็บไว้เป็น fillable AcroForm widgets หรือ False เพื่อทำเครื่องหมาย widget ทุกตัวว่า read-only ให้ output เป็นบันทึกที่ frozen rich-text draw blocks ที่มี styled runs และ link annotations ถูกสร้างขึ้นไม่ว่าจะด้วยวิธีใด function คืนจำนวน widgets ที่ emit

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True เก็บฟิลด์ให้กรอกได้; False ตรึงให้อ่านอย่างเดียว
    Emitted := Pdf.FlattenLoadedXFA(True);

    // สิ่งใดที่เครื่องมือจับคู่ไม่ได้จะถูกรายงาน ไม่ใช่ถูก raise
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

อ่าน XFAFlattenWarnings เสมอหลังจากการเรียก รายการถูกล้างที่จุดเริ่มต้นของ flatten แต่ละครั้งและสะสมบรรทัดสำหรับทุก element ที่ engine ปฏิเสธที่จะ render: field kind ที่ไม่รองรับ, draw image ที่ไม่สามารถ decode ได้, exData block ที่ไม่มี spans ที่ใช้ได้ ไม่มีสิ่งใดเหล่านั้น raise exception ดังนั้นรายการ warnings ว่างเปล่าคือหลักฐานว่าทุกอย่าง map และรายการที่ไม่ว่างเปล่าบอกคุณว่าต้นฉบับใดที่ควรตรวจสอบ เมื่อคุณมี raw XFA เป็น XDP bytes แทนที่จะเป็น PDF ที่โหลดแล้ว sibling method ApplyXFAAsAcroForm รับ bytes เหล่านั้นโดยตรงและใช้ code path เดียวกันและพฤติกรรม warnings เดียวกัน method ที่เสริมกัน AddXFAPacket ทำตรงข้าม โดยฝัง XFA packet ลงในเอกสารที่คุณกำลังสร้าง

การยืนยันผลลัพธ์ใน reader

เปิดไฟล์ที่ flatten แล้วใน Acrobat หรือ viewer ปัจจุบันใดก็ได้ และตรวจสอบสองสิ่ง ก่อนคือ rich text render พร้อม styling ที่สมบูรณ์: runs ที่ bold ยังคง bold, runs ที่มี color มีสี และ spans อยู่ในลำดับที่ถูกต้องบนบรรทัดแทนที่จะทับซ้อนกันหรือวิ่งออกนอก box ประการที่สองคือ hyperlinks ใช้งานได้จริง hover บน anchor และ status bar ควรแสดง target address, คลิกและ URI action ควรเปิดมัน ใช้ annotation inspector ของ viewer เพื่อยืนยันว่าแต่ละอันเป็น /Link annotation จริง ๆ ที่ /Rect กอด anchor text อยู่บน content ที่ตอนนี้เป็น plain painted glyphs ไม่ใช่ form-rendered XFA ชุดนั้นซึ่งก็คือ styled static text บวก real Link annotations บน rectangles ที่ถูกต้องคือสิ่งที่ทำให้เอกสารที่ flatten แล้วมีชีวิตยืนยาวกว่า XFA engines ที่มันไม่ต้องการอีกต่อไป

การ flatten fields เอง ซึ่งก็คือ text boxes, check boxes และ choice lists ที่ล้อมรอบ rich text นี้ ครอบคลุมอยู่ใน คู่มือของเราเกี่ยวกับการ flatten XFA forms เป็น AcroForm widgets สำหรับเรื่องราวที่กว้างขึ้นของการสร้างและวาง Link annotations ด้วยตนเอง นอกเหนือจากที่ flatten path สร้างขึ้น ดู การทำงานกับ PDF annotations ใน HotPDF ทั้งสองสร้างบน annotation และ forms model เดียวกับที่ ship มาพร้อมกับ HotPDF Delphi Component สำหรับ Delphi และ C++Builder