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

เลย์เอาต์ PDF เชิงประกาศใน Delphi พร้อมผลลัพธ์แบบแท็ก

HotPDF สามารถสร้างเอกสารแบบแบ่งหน้าจากโครงสร้างต้นไม้เชิงประกาศแทนที่จะใช้พิกัดได้ คุณประกอบ THPDFDOMDocument ขึ้นจากส่วน (section) สแตก ข้อความ รายการ และตาราง แล้วส่งต่อให้ THPDFDOMRenderer จากนั้นตัวเรนเดอร์จะวัดขนาด แบ่งหน้า วาดองค์ประกอบประจำหน้า และเมื่อได้รับคำสั่งก็จะสร้างโครงสร้างต้นไม้ PDF/UA ที่ทำให้ผลลัพธ์เข้าถึงได้ด้วย โค้ดเลย์เอาต์ไม่เคยต้องคำนวณพิกัด y เลย

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

โครงสร้างต้นไม้เป็นเจ้าของอะไร และเหตุใดกฎการเป็นเจ้าของจึงเข้มงวด

DOM บังคับใช้กฎการเป็นเจ้าของเพียงหนึ่งเดียวในทุกระดับ เอกสารเป็นเจ้าของส่วนต่างๆ ของมัน ส่วนหนึ่งเป็นเจ้าของเนื้อหา หัวกระดาษ และท้ายกระดาษของมัน และสแตก คอนเทนเนอร์ และตารางเป็นเจ้าของลูกของมัน การใช้ซ้ำเกิดขึ้นผ่าน Clone หรือผ่านโรงงาน (factory) ที่ลงทะเบียนไว้เท่านั้น ไม่มีการแนบอ็อบเจกต์เดียวกันเข้ากับพาเรนต์สองตัวเด็ดขาด กฎนี้ไม่ใช่พิธีกรรมเปล่าๆ คอมโพเนนต์ที่ปรากฏสองครั้งในโครงสร้างต้นไม้จะถูกวัดขนาดสองครั้งด้วยเงื่อนไขที่ต่างกัน และถูกปลดปล่อยหน่วยความจำสองครั้งเมื่อรื้อทิ้ง

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

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // สร้างโครงสร้างต้นไม้ PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 หน่วยพอยต์
  Section.PageHeight := 842;
  Section.MarginLeft := 56;
  Section.MarginTop := 56;
  Section.MarginRight := 56;
  Section.MarginBottom := 56;
  Section.Style.FontName := 'Helvetica';
  Section.Style.FontSize := 10;

  Section.Body.AddHeading('Annual maintenance report', 1);
  Section.Body.AddText('Every asset inspected during the reporting ' +
    'period is listed below, grouped by site.');
  Section.Body.AddSpacer(12);

  Table := THPDFDOMTable.Create('assets');
  Table.AddColumn(3);                 // น้ำหนัก ไม่ใช่ความกว้างสัมบูรณ์
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // แถวหัวตาราง
  Row[0].Text := 'Asset';
  Row[1].Text := 'Last service';
  Row[2].Text := 'Status';
  for I := 0 to High(Assets) do
  begin
    Row := Table.AddRow(16);
    Row[0].Text := Assets[I].Name;
    Row[1].Text := Assets[I].ServiceDate;
    Row[2].Text := Assets[I].Status;
  end;
  Section.Body.Add(Table);
end;

การแบ่งหน้าหลีกเลี่ยงต้นทุนกำลังสองได้อย่างไร

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

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

การวัดขนาดยังคงซื่อตรงต่อผลข้างเคียง THPDFLayoutElement.Measure ถูกกำหนดให้ต้องปราศจากผลข้างเคียงด้านการวาด และการวางตำแหน่งจริงจะรันผ่าน THotPDF.PlaceLayoutElement เสมอ ซึ่งเป็นรูทีนกลางเดียวกันที่วัดขนาดชิ้นส่วนที่วางแล้วซ้ำ ตั้งค่าความเป็นเจ้าของส่วนล้น และบันทึกข้อมูลวินิจฉัย ตัวเรนเดอร์ DOM ตัดสินใจเพียงนโยบายหน้าใหม่ องค์ประกอบประจำหน้า ระยะห่าง และอายุการใช้งานของการต่อเนื่องเท่านั้น

กฎหัวตารางที่ป้องกันเอกสารไม่รู้จบ

การซ้ำหัวตารางข้ามหน้าฟังดูง่าย แต่ซ่อนโหมดความล้มเหลวไว้สองแบบ HotPDF กำหนดว่าแถวหัวตารางต้องปรากฏเฉพาะในชุดแถวต่อเนื่องชุดแรกเท่านั้น และการแบ่งครั้งแรกต้องใส่แถวหัวตารางทั้งหมดบวกกับแถวเนื้อหาอย่างน้อยหนึ่งแถวให้พอดี หากไม่มีกฎข้อที่สอง หัวตารางที่สูงกว่าพื้นที่ที่เหลืออยู่จะสร้างหน้าที่มีแต่หัวตารางล้วนๆ ตามด้วยหน้าที่เหมือนกันทุกประการอีกหน้าหนึ่งไปเรื่อยๆ ไม่รู้จบ

หน้าที่ต่อเนื่องจะวาดหัวตารางซ้ำ และสำเนาที่วาดซ้ำนั้นจะถูกทำเครื่องหมายเป็นสิ่งประดิษฐ์ (artifact) ไม่ใช่เนื้อหา ซึ่งเป็นคำตอบที่ถูกต้องทั้งสำหรับการเข้าถึงและการสกัดข้อความ แถวหัวตารางดั้งเดิมยังคงอยู่ในโครงสร้างตารางเชิงตรรกะเพียงครั้งเดียวเท่านั้น หากข้ามขั้นตอนนี้ไป โปรแกรมอ่านหน้าจอจะประกาศชื่อคอลัมน์ซ้ำอีกครั้งกลางข้อมูล และตัวสกัดข้อความก็จะแทรกแถวหัวตารางซ้ำซ้อนเข้าไปในระหว่างแถวเนื้อหา

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

หนึ่งอิลิเมนต์เชิงตรรกะ หลายชิ้นส่วนหน้า

การแท็กอัตโนมัติคือจุดที่แบบจำลองการแบ่งหน้าและแบบจำลองโครงสร้างต้องสอดคล้องกัน ย่อหน้าที่ถูกแบ่งข้ามสองหน้าคือย่อหน้าเชิงตรรกะเดียว ดังนั้นจึงต้องคงเป็นอิลิเมนต์โครงสร้างเดียว แต่ตัวระบุเนื้อหาที่ทำเครื่องหมาย (marked content) นั้นเป็นรายหน้า ดังนั้นแต่ละชิ้นส่วนที่มองเห็นจึงต้องมี MCID ของตัวเองบนหน้าที่มันปรากฏ

HotPDF แก้ปัญหานี้ด้วยการคงอิลิเมนต์โครงสร้างเดียวไว้ และต่อท้ายการอ้างอิงเนื้อหาที่ทำเครื่องหมายเข้าไปในอาร์เรย์ /K ของมันสำหรับแต่ละชิ้นส่วน โดยมีคู่ /Pg และ /MCID ระบุหน้าและตัวระบุ ช่อง ParentTree สำหรับ MCID นั้นชี้กลับไปยังอิลิเมนต์เดียวกัน นี่คือสิ่งที่ ISO 14289 คาดหวังพอดี และเป็นเหตุผลที่การโคลนแบบต่อเนื่องแตกต่างจากการโคลนธรรมดา การ Clone ธรรมดาหมายถึงเนื้อหาเชิงตรรกะใหม่และได้รับเอกลักษณ์เชิงความหมายใหม่ ในขณะที่การโคลนแบบต่อเนื่องภายในจะสืบทอดเอกลักษณ์ของคอมโพเนนต์ที่มันต่อเนื่องมา

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

กฎโครงสร้างที่ตัวเรนเดอร์บังคับใช้ล่วงหน้า

เมื่อเปิดใช้งาน GenerateStructure กฎ PDF/UA หลายข้อจะถูกตรวจสอบขณะที่โครงสร้างต้นไม้กำลังถูกเรนเดอร์ ไม่ใช่หลังจากไฟล์มีอยู่แล้ว หัวเรื่องเริ่มที่ระดับ 1 และข้ามระดับไม่ได้ LI ปรากฏได้เฉพาะภายใน L เท่านั้น และ Lbl กับ LBody ปรากฏได้เฉพาะภายใน LI เท่านั้น TR เป็นของตาราง และ TH กับ TD เป็นของแถว รูปภาพที่ไม่มีข้อความทางเลือกจะถูกปฏิเสธในโหมด PDF/UA

การปฏิเสธตั้งแต่เนิ่นๆ คือทางเลือกที่จงใจในที่นี้ ตัววาลิเดตที่รายงานว่าขาดข้อความทางเลือกหลังจากเอกสารถูกเขียนแล้วบอกคุณเพียงว่ารายการใบแจ้งยอดหนึ่งหมื่นรายการต้องสร้างใหม่ ส่วนตัวเรนเดอร์ที่ปฏิเสธคอมโพเนนต์นั้นบอกคุณว่าคอมโพเนนต์ตัวไหน ในขณะที่ข้อมูลที่สร้างมันขึ้นมายังคงอยู่ในขอบเขตการทำงาน การตรวจสอบความสอดคล้องตามมาตรฐานยังคงเป็นขั้นตอนแยกต่างหากในไปป์ไลน์ และกลไกของมันอธิบายไว้ในการตรวจสอบ PDF/A, PDF/X และ PDF/UA

var
  Pdf: THotPDF;
  Renderer: THPDFDOMRenderer;
  Stats: THPDFDOMRenderStatistics;
begin
  Pdf := THotPDF.Create(nil);
  Renderer := THPDFDOMRenderer.Create;
  try
    Pdf.FileName := 'maintenance-report.pdf';
    Pdf.BeginDoc;
    Stats := Renderer.Render(Doc, Pdf);
    Pdf.EndDoc;

    Writeln(Format('%d page(s), %d placement(s), %d split(s)',
      [Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
    Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
      [Stats.StructureElementCount, Stats.MarkedContentCount,
       Stats.ArtifactCount]));
    Writeln(Format('deepest continuation chain: %d',
      [Stats.MaximumContinuationDepth]));
  finally
    Renderer.Free;
    Doc.Free;
    Pdf.Free;
  end;
end;

ระเบียนสถิตินี้มีประโยชน์มากกว่าที่เห็นในตอนแรก SplitCount ที่เพิ่มขึ้นอย่างรวดเร็วหลังการเปลี่ยนแปลงเทมเพลตมักหมายความว่าคอมโพเนนต์เริ่มวัดขนาดสูงกว่าคอนเทนเนอร์ของมัน MaximumContinuationDepth ที่ค่อยๆ เพิ่มขึ้นเป็นสัญญาณเตือนล่วงหน้าสำหรับคอมโพเนนต์ที่ Split ของมันคืบหน้าน้อยเกินไปต่อหนึ่งหน้า และการเปรียบเทียบ ArtifactCount กับจำนวนหน้าต่อเนื่องช่วยยืนยันว่าหัวตารางที่ซ้ำจริงๆ ถูกแท็กเป็นสิ่งประดิษฐ์แล้ว

DOM เข้ากันได้อย่างไรกับ API แบบตรง

DOM ไม่ได้แทนที่การวาดแบบตรง แต่วางอยู่บนอ็อบเจกต์หน้าเดียวกัน สิ่งใดก็ตามที่ตัวเรนเดอร์วางไว้สามารถแทรกสลับกับการเรียกแบบตรงบน THotPDF ได้ ซึ่งสำคัญเมื่อรายงานต้องการอิลิเมนต์ที่วางตำแหน่งด้วยมือหนึ่งชิ้น เช่น ภาพลายเซ็นในตำแหน่งที่แน่นอน การปิดหน้ายังคงอยู่ภายใต้การควบคุมของ AddPage และ EndDoc ดังนั้นโหมด flush ทันทีจะไม่เก็บหน้าที่เสร็จแล้วไว้ในหน่วยความจำ และหน่วยความจำที่ใช้งานอยู่ยังคงถูกควบคุมโดยการต่อเนื่องปัจจุบัน ทรัพยากรฟอนต์ และกราฟอ็อบเจกต์เอกสารตามปกติ

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

เลย์เอาต์เชิงประกาศ การแท็กอัตโนมัติ และ API การวาดแบบตรง มาพร้อมกันในคอมโพเนนต์เดียวกันสำหรับ Delphi และ C++Builder รายการคุณสมบัติทั้งหมดอยู่ที่หน้าคอมโพเนนต์ PDF ของ HotPDF สำหรับ Delphi