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

จัดเรียงเนื้อหา PDF ใหม่เป็น HTML ที่ตอบสนองได้ใน Delphi

PDFium Component แปลง PDF ที่มีเลย์เอาต์คงที่ให้เป็นแบบจำลองเชิงความหมายที่สามารถจัดเรียงใหม่ได้ ด้วยการใช้ BuildReflowDocument และส่งออกแบบจำลองนั้นเป็น HTML แบบพึ่งพาตัวเองผ่าน ToHtml หัวเรื่องยังคงเป็นหัวเรื่อง รายการยังคงเป็นรายการ และตารางที่ตรวจพบบนหน้าจะออกมาเป็นมาร์กอัปตารางจริง โดยคงเซลล์หัวตารางและการรวมเซลล์ไว้ ไม่มีสิ่งใดในผลลัพธ์อ้างอิงสคริปต์หรือสไตล์ชีตภายนอกเลย

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

ข้อมูลเชิงความหมายมาจากไหน

ทุกอย่างเริ่มต้นจาก GetStructuredText ซึ่งเป็นแหล่งข้อความและความหมายเดียวในคอมโพเนนต์ เมื่อ PDF พกโครงสร้างต้นไม้ไว้ คือ tagged PDF ตามที่นิยามไว้ใน ISO 32000-1 หัวข้อ 14.7 แบบจำลองจะปฏิบัติตามลำดับชั้นเชิงตรรกะที่ตัวสร้างบันทึกไว้ เมื่อไม่มี และ PDF ส่วนใหญ่ในโลกจริงไม่มี แบบจำลองจะถอยกลับไปใช้ลำดับเลย์เอาต์ทางกายภาพที่คำนวณไว้แล้วเพื่อวัตถุประสงค์ด้านลำดับการอ่าน

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

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

โครงสร้างต้นไม้แบบแบน และเหตุใดจึงไม่ใช่ต้นไม้ของอ็อบเจกต์

แบบจำลองเป็นโครงสร้างต้นไม้ที่คลี่แบนแบบ pre-order คืออาร์เรย์ของโหนดที่แต่ละโหนดพก ParentIndex และ Depth แทนที่จะเป็นระเบียนแบบเรียกซ้ำหรือกราฟอ็อบเจกต์ที่มีความเป็นเจ้าของ หน้า หัวเรื่อง ย่อหน้า รายการ รายการย่อย รูปภาพ คำบรรยายภาพ ตาราง แถว และเซลล์ ล้วนอยู่ในอาร์เรย์เชิงเส้นเดียวนั้น

ประโยชน์สองอย่างตามมา ผู้ใช้งานสามารถสตรีมอาร์เรย์ตามลำดับได้โดยไม่ต้องเรียกซ้ำ ซึ่งทำให้การสร้าง HTML, Markdown หรือมุมมองต้นไม้เป็นเพียงลูปธรรมดา และเลย์เอาต์ยังคงพกพาข้ามได้ระหว่าง Delphi, C++Builder และ Free Pascal ซึ่งแตกต่างกันในวิธีจัดการชนิดข้อมูลที่มีการจัดการหน่วยความจำแบบเรียกซ้ำข้ามขอบเขต ABI ระเบียนแบบเรียกซ้ำของอาร์เรย์ไดนามิกคือโครงสร้างแบบที่คอมไพล์ได้ทุกที่แต่มีพฤติกรรมต่างกันเล็กน้อยในแต่ละที่พอดี

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfReflowOptions;
  Doc: TPdfReflowDocument;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.LoadDocument;

    Options := TPdfReflowOptions.Default;
    Options.FullDocument := True;
    Options.DetectTables := True;
    Options.IncludeCss := True;          // inline style block, no external file
    Options.MaxNodes := 200000;          // fail-closed budget
    Options.MaxCharacters := 4000000;

    Doc := Pdf.BuildReflowDocument(Options);

    for I := 0 to High(Doc.Nodes) do
      case Doc.Nodes[I].Kind of
        prnkHeading:
          Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
        prnkParagraph:
          Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Copy(Doc.Nodes[I].Text, 1, 60)]));
        prnkTable:
          Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
      end;

    Writeln(Format('%d node(s), %d table(s), %d character(s)',
      [Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
  finally
    Pdf.Free;
  end;
end;

ตารางถูกป้องกันไม่ให้ปรากฏซ้ำสองครั้งได้อย่างไร

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

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

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

การส่งออก HTML ที่ยังคงพึ่งพาตัวเองได้

ToHtml ไล่เดินแบบจำลองที่สร้างไว้แล้วและไม่เคยกลับไปหา PDFium อีก ดังนั้นการส่งออกสองครั้งจึงไม่มีต้นทุนเพิ่มเติม และไม่สามารถให้ผลลัพธ์ต่างกันจากแบบจำลองเดียวกันได้ ข้อความและค่าแอตทริบิวต์ถูกเอสเคปอย่างสม่ำเสมอ ระดับหัวเรื่องถูกจำกัดให้อยู่ในช่วง h1 ถึง h6 ที่ HTML นิยามไว้จริง และเซลล์หัวตาราง RowSpan และ ColumnSpan ผ่านไปตามที่เขียนไว้

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

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // keep page boundaries visible
  Options.PreserveLineBreaks := False;   // let the browser wrap paragraphs

  Html := Pdf.BuildReflowDocument(Options).ToHtml;

  Bytes := TEncoding.UTF8.GetBytes(string(Html));
  Stream := TFileStream.Create('report.html', fmCreate);
  try
    if Length(Bytes) > 0 then
      Stream.WriteBuffer(Bytes[0], Length(Bytes));
  finally
    Stream.Free;
  end;
end;

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

งบประมาณ การยกเลิก และสถานะหน้า

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

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

การจัดเรียงใหม่เหมาะกับอะไร และไม่เหมาะกับอะไร

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

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

การจัดเรียงใหม่ ข้อความเชิงโครงสร้าง การตรวจสอบการแท็ก และการเรนเดอร์ ใช้อ็อบเจกต์เอกสารเดียวกันร่วมกันข้าม Delphi, C++Builder และ Lazarus API ทั้งหมดอธิบายไว้ที่หน้าคอมโพเนนต์ PDFium สำหรับ Delphi