HotPDF เขียนไฟล์ PDF แบบ linearized layout ที่ Acrobat เรียกว่า Fast Web View ผ่าน property LinearizeOutput บน THotPDF การตั้งค่านี้ก่อน BeginDoc จะทำให้ HotPDF จัดเรียง object graph ที่เสร็จสมบูรณ์ใหม่ เพื่อให้ตัวอ่านที่รู้จัก byte-range สามารถแสดงหน้าแรกได้หลังจากดึงข้อมูลแค่ส่วนต้นของไฟล์ แทนที่จะต้องดาวน์โหลดทั้งเอกสารก่อน กลไกนี้คือ ISO 32000-1 Annex F
เหตุผลที่เรื่องนี้สำคัญนั้นไม่หวือหวาเลย PDF ปกติจะวาง cross-reference table ไว้ท้ายไฟล์ ดังนั้นตัวอ่านต้องไปถึงไบต์สุดท้ายก่อนที่มันจะรู้ว่าอะไรอยู่ตรงไหน ส่งรายงานสแกน 200 หน้าให้เบราว์เซอร์ ผู้ใช้ก็ต้องจ้องมอง spinner รอการโอนข้อมูลทั้งหมด ทั้งที่สิ่งเดียวที่พวกเขาต้องการคือหน้า 1 Linearization แก้ปัญหานี้โดยจ่ายต้นทุนตอนเขียนไฟล์ บทความนี้พูดถึงเส้นทางการเขียนนั้นโดยเฉพาะ การแบ่งส่วน ลูปการวัดผล และขีดจำกัดที่แน่นอน สำหรับพื้นฐานเชิงแนวคิดว่า Fast Web View ให้ประโยชน์อะไร บทความก่อนหน้าเรื่อง คำอธิบายเรื่อง PDF linearization และ Fast Web View ครอบคลุมพื้นฐานนั้น
Layout แบบ linearized รับประกันอะไรจริง ๆ
ไฟล์แบบ linearized คือ PDF ธรรมดาที่มีลำดับทางกายภาพเฉพาะเจาะจงมาก และทุกการรับประกันที่มันให้มาจากลำดับนั้น ไม่ใช่จาก object type ใหม่ใด ๆ HotPDF ส่งออกแต่ละส่วนตามลำดับที่ Annex F กำหนด คือ linearization parameter dictionary ภายใน 1024 ไบต์แรก, cross-reference table แบบต้น, object ระดับเอกสาร, primary hint stream, หน้าแรกและ object ส่วนตัวของมัน, หน้าที่เหลือ, object ที่ใช้ร่วมกัน, ทุกอย่างที่เหลือ และสุดท้าย main cross-reference table
การแบ่งส่วนถูกอนุมาน ไม่ใช่ถูกประกาศ HotPDF เดินตามกราฟ reference จาก object ของแต่ละหน้า และบันทึกไว้สำหรับทุก indirect object ว่ามีกี่หน้าที่เข้าถึงมันได้ และหน้าไหนเข้าถึงมันเป็นหน้าแรก object ที่ใช้โดยหน้าเดียวเท่านั้นจะกลายเป็นส่วนตัวของหน้านั้น object ที่เข้าถึงได้จากมากกว่าหนึ่งหน้าจะกลายเป็นแบบใช้ร่วมกัน catalog พร้อมกับสิ่งที่มันอ้างอิงภายใต้ /ViewerPreferences, /OpenAction, /Threads และ /AcroForm รวมถึง encryption dictionary เมื่อการป้องกันเปิดใช้งานอยู่ จะรวมกันเป็นกลุ่มระดับเอกสารที่ต้องมาก่อนทุกอย่าง page tree node ถูกกันไว้อย่างตั้งใจเพื่อไม่ให้ปนเปื้อนส่วนหน้าแรก
parameter dictionary พกตัวเลขที่ตัวอ่านต้องการก่อนที่จะอ่านอะไรอื่นเลย /L สำหรับความยาวไฟล์รวม /H สำหรับ offset และความยาวของ hint stream /O สำหรับ object number ของหน้าแรก /E สำหรับไบต์ที่ส่วนหน้าแรกจบลง /N สำหรับจำนวนหน้า และ /T สำหรับ offset ของ entry ใน main cross-reference table ทุกตัวในนี้คือ byte offset เข้าไปในไฟล์ที่ยังไม่มีอยู่ ณ ขณะที่คุณต้องเขียนมัน
ทำไม offset ของ hint table ต้องลู่เข้าหากัน
เพราะตัวเลขใน parameter dictionary อธิบายไฟล์ที่มีมันเองอยู่ในนั้น และการเปลี่ยนตัวเลขใดตัวหนึ่งก็เปลี่ยนไฟล์ นี่คือความยากหลักของตัวเขียนแบบ linearized และเป็นเหตุผลที่ HotPDF วัดผลซ้ำ ๆ แทนที่จะเขียนครั้งเดียว ขยาย /T จาก 6 หลักเป็น 7 หลัก parameter dictionary จะโตขึ้นหนึ่งไบต์ header จะโตขึ้น object ทุกตัวจะเลื่อน main cross-reference table จะย้าย /T ก็ต้องการค่าใหม่อีก layout ต้องไปถึงจุดคงที่ก่อนที่จะมีการเขียนไบต์จริงแม้แต่ไบต์เดียว
HotPDF จัดการเรื่องนี้ด้วยการวนซ้ำแบบมีขอบเขต มันจะ serialize object ทุกตัวเข้าไปใน counting stream ที่บันทึกความยาวโดยไม่เก็บไบต์ก่อน ดังนั้นแต่ละ object จึงมีขนาด serialized ที่รู้ค่าแล้ว จากนั้นมันจะรัน layout pass ที่กำหนด offset ให้กับกลุ่มระดับเอกสาร, hint stream, กลุ่มหน้าแรก, กลุ่มหน้าถัดไป, กลุ่มที่ใช้ร่วมกัน และส่วนที่เหลือ และรายงานว่า main cross-reference table จะไปตกอยู่ตรงไหน ผลลัพธ์นั้นจะถูกป้อนกลับเป็นข้อมูลนำเข้าของรอบถัดไป ลูปนี้จำกัดไว้ที่แปดครั้ง และการไม่ลู่เข้าจะทำให้เกิด exception แทนที่จะสร้างไฟล์ที่มี offset ผิดแต่ดูสมเหตุสมผล
CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
CalculateLayout(CandidateMainOffset, FirstXRefData,
HintOffset, EndFirstPage, NewMainOffset);
if NewMainOffset = CandidateMainOffset then
Break;
CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
raise Exception.Create('Linearization layout did not converge');
รายละเอียดสองอย่างช่วยไม่ให้ลูปนี้กวัดแกว่ง parameter dictionary ถูกเขียนลงในช่อง 384 ไบต์คงที่ เติมด้วยช่องว่าง ดังนั้นการเติบโตของมันเองจึงไม่มีทางทำให้ layout ไม่เสถียร ถ้าข้อความ dictionary เคยยาวเกินการจองไว้นั้น HotPDF จะ raise แทนที่จะเลื่อนทุกอย่างอย่างเงียบ ๆ และหลังจากลู่เข้าแล้ว HotPDF จะรัน layout pass ยืนยันอีกหนึ่งรอบและตรวจสอบความยาวของ hint stream อีกครั้ง เพราะ hint stream เองก็เข้ารหัส offset ที่รู้ค่าแน่นอนได้ก็ต่อเมื่อ layout ลงตัวแล้ว ผลตอบแทนของการวัดผลทั้งหมดนี้คือ HotPDF ไม่เคย buffer สำเนาที่สองของเอกสารเลย เมื่อ offset ถูกกำหนดแล้ว object จะถูก serialize ตรงเข้าไปยัง destination stream พร้อม assertion ที่ขอบเขตของแต่ละส่วนว่าไบต์ที่เขียนตรงกับ offset ที่สัญญาไว้
การเปิดใช้งานจาก Delphi
API surface มีแค่ Boolean ตัวเดียว และข้อกำหนดเดียวคือต้องตั้งค่ามันก่อนเริ่มการสร้าง LinearizeOutput มีค่าเริ่มต้นเป็น False และ layout pass จะรันตอนที่เอกสารถูกเขียน ดังนั้นการกำหนดค่าหลัง EndDoc จะไม่มีผลอะไรเลย
var
PDF: THotPDF;
begin
PDF := THotPDF.Create(nil);
try
PDF.FileName := 'fast-view.pdf';
PDF.Version := pdf17;
PDF.LinearizeOutput := True; // must precede BeginDoc
PDF.BeginDoc;
PDF.Canvas.TextOut(72, 72, 'First page');
PDF.EndDoc;
finally
PDF.Free;
end;
end;
มีข้อควรระวังหนึ่งเรื่องการ deploy ที่สำคัญกว่าทุกอย่างในฝั่งโค้ด Linearization จะคุ้มค่าก็ต่อเมื่อ transport รองรับ HTTP range request เสิร์ฟไฟล์เดียวกันจาก endpoint ที่ stream ทั้งหมด หรือจาก CDN configuration ที่ไม่สนใจ Range คุณก็เพียงแค่ซื้อเส้นทางการเขียนที่ช้าลงและไฟล์ใหญ่ขึ้นโดยไม่มีประโยชน์ใดที่ผู้ใช้เห็นได้ ตรวจสอบเซิร์ฟเวอร์ก่อนตรวจสอบโค้ด
ทำไม linearization ถึง override UseXRefStream และ UseObjectStreams
เพราะตัวเขียนแบบ linearized ต้องการให้ทุก object มี byte offset ที่ระบุตำแหน่งได้โดยตรงเป็นของตัวเอง และทั้งสองฟีเจอร์นั้นเอาความสามารถนั้นไป HotPDF จึงส่งออกตาราง cross-reference แบบข้อความดั้งเดิมและ indirect object แบบไม่บีบอัดทุกครั้งที่ LinearizeOutput เปิดใช้งาน แม้ว่าผู้เรียกจะตั้งค่า UseXRefStream หรือ UseObjectStreams ไว้ก็ตาม นี่เป็นการ override โดยตั้งใจ ไม่ใช่ความขัดแย้งที่คุณต้องแก้เอง
เหตุผลนี้ตามมาจาก hint table hint table อธิบายว่าส่วนของหน้าเริ่มตรงไหนและยาวเท่าไร เพื่อให้ตัวอ่านขอช่วงนั้นได้พอดี object ที่ถูกอัดใน container /ObjStm ไม่มี offset อิสระเลย มันดำรงอยู่แค่เป็นชิ้นส่วนภายใน stream ที่บีบอัดอีกตัวหนึ่งที่ต้องถูกดึงและคลายบีบอัดทั้งก้อน ถ้าคุณหวังพึ่ง object stream เพื่อลดขนาดไฟล์ ควรเข้าใจว่า linearization กับการบีบอัดกำลังดึงกันไปคนละทาง และควรอ่านการแลกเปลี่ยนนี้ในบทความคู่หูเรื่อง object stream และการอัปเดตแบบ incremental ใน HotPDF ความตึงเครียดแบบเดียวกันนี้กำหนดรูปแบบของไฟล์แบบ hybrid-reference ซึ่งมีไว้เพื่อให้ตัวอ่านรุ่นเก่ายังทำงานได้ควบคู่กับตารางแบบ stream ตามที่กล่าวไว้ในบทความเรื่อง hybrid cross-reference stream ในไฟล์ PDF ที่สร้างจาก Office
ยังมีขีดจำกัดของเวอร์ชันด้วย Linearization ต้องใช้ PDF 1.2 หรือใหม่กว่า ถ้าเวอร์ชันที่เลือกเก่ากว่านั้น HotPDF จะยกระดับมันขึ้นให้อัตโนมัติ เว้นแต่ตั้ง StrictVersionLock ไว้ ซึ่งในกรณีนั้นการเขียนจะทำให้เกิด exception แทนที่จะเลื่อนเวอร์ชันเอกสารที่คุณตรึงไว้อย่างตั้งใจโดยเงียบ ๆ
กำแพง 4 GiB และทำไม HotPDF ปฏิเสธแทนที่จะตัดทอน
Hint table ของ linearization เก็บ offset เป็นค่า 32-bit ดังนั้นไฟล์แบบ linearized จึงระบุตำแหน่งสิ่งใดที่ 4 GiB หรือมากกว่าไม่ได้ และ HotPDF จะปฏิเสธผลลัพธ์เช่นนั้นด้วย exception ที่ชัดเจน แทนที่จะเขียนไฟล์ที่มี offset ที่วนกลับ ขีดจำกัดนี้ไม่ใช่ทางเลือกของการทำงาน HotPDF แต่เป็นความกว้างของฟิลด์ที่ Annex F กำหนดไว้
การตรวจสอบนี้ถูกใช้ในสามจุด และทั้งสามจุดล้วนสำคัญ HotPDF ตรวจสอบแต่ละ object เมื่อรู้ความยาว serialized แล้ว ตรวจสอบความยาวของแต่ละส่วนของหน้าขณะสร้าง hint entry และตรวจสอบความยาวไฟล์สุดท้ายหลังจาก main cross-reference table ถูกกำหนดขนาดแล้ว การล้มเหลวตั้งแต่เนิ่น ๆ คือจุดสำคัญทั้งหมด hint table ที่มี offset ถูกตัดทอนอย่างเงียบ ๆ จะสร้างไฟล์ที่เปิดได้ถูกต้องในตัวอ่านที่ดาวน์โหลดทั้งไฟล์ แต่ล้มเหลวเฉพาะกับ byte-range client ที่ linearization มีไว้ให้บริการ ซึ่งเป็นรูปแบบความล้มเหลวที่เลวร้ายที่สุดเท่าที่จะเป็นไปได้ เพราะตัวอ่านทดสอบของคุณไม่มีวันเจอปัญหานี้เลย ถ้าคุณกำลังสร้างผลลัพธ์ขนาดหลายกิกะไบต์ linearization ไม่ใช่เครื่องมือที่เหมาะสม และแนวทาง streaming ที่กล่าวไว้ในบันทึกเรื่อง Direct File API สำหรับเวิร์กโฟลว์ PDF ขนาดใหญ่ คือทิศทางที่ควรมองหา
การตรวจจับ linearization ในไฟล์ที่คุณโหลด
THotPDF.IsLoadedLinearized รายงานว่าเอกสารที่โหลดอยู่ในปัจจุบันเคยถูกเขียนในรูปแบบ linearized มาแล้วหรือไม่ และคำตอบมาจาก snapshot ที่ถ่ายไว้ก่อน parse ไม่ใช่จาก stream ที่มีชีวิตอยู่ HotPDF อ่าน 1024 ไบต์แรกจากตำแหน่งศูนย์ของ source stream สแกนหา keyword obj ตัวแรก แล้วหา entry /Linearized ที่มีค่า 1 และ cache ผลลัพธ์ boolean ไว้
var
PDF: THotPDF;
PageCount: Integer;
begin
PDF := THotPDF.Create(nil);
try
PageCount := PDF.LoadFromFile('incoming.pdf');
if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
Writeln('Source is not Fast Web View ready');
finally
PDF.Free;
end;
end;
ข้อจำกัดสองอย่างในคำอธิบายนี้สำคัญมาก การตรวจจับพึ่งพาตำแหน่ง stream ไม่ได้ เพราะเมื่อโค้ดแอปพลิเคชันถามคำถามนี้ ตัว parser ได้เลื่อนมันไปแล้ว และมันอ่านซ้ำตามต้องการไม่ได้ เพราะ LoadFromFile ปล่อย source stream ภายในทิ้งเมื่อการโหลดเสร็จสิ้น จึงเป็นเหตุผลของการออกแบบแบบจับก่อน-parse-แล้ว-cache การสแกนยังตั้งใจให้อ่านค่าแบบตรงตัวด้วย มีแค่ /Linearized 1 หรือรูปแบบเทียบเท่าทางตัวเลขที่ส่วนทศนิยมเป็นศูนย์ทั้งหมดเท่านั้นที่ยอมรับ เพราะไฟล์ที่ parameter dictionary ระบุอย่างอื่นไม่ได้ทำตามสัญญาของ Annex F
กับดัก record ของ Delphi ที่ควรลอกไปใช้
Record แบบ local ที่มี dynamic array จะกำหนดค่าเริ่มต้นให้เฉพาะฟิลด์ที่จัดการ (managed) ของมันเท่านั้น และไม่มีอะไรอื่น ถ้าคุณเก็บฟิลด์ Count ธรรมดาไว้ข้าง ๆ array คุณต้อง clear มันเองด้วย ปัญหานี้เคยกัด linearization partitioning ระหว่างการพัฒนา และเป็นบั๊กประเภทที่เสียเวลาหนึ่งวันพอดี เพราะแพลตฟอร์มหนึ่งซ่อนมันไว้
type
THPDFLinearIndexList = record
Values: THPDFIntegerArray; // managed field: cleared for you
Count: Integer; // plain field: whatever was on the stack
end;
// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);
ฟิลด์ dynamic array นับ reference count ดังนั้น compiler จะเซ็ตให้เป็นศูนย์ให้ ส่วน Count ที่อยู่ข้าง ๆ มันคือ integer ธรรมดาที่ไม่มีการรับประกันแบบนั้น Count ที่ไม่ได้กำหนดค่าเริ่มต้นจะส่งการ append ครั้งแรกไปยังดัชนีที่สุ่มไปเรื่อย ๆ บน Win32 ช่อง stack นั้นบังเอิญมีค่าเป็นศูนย์ การ append จึงลงที่ index 0 พอดี และทุกการทดสอบก็ผ่านหมด บน Win64 โค้ดชุดเดียวกันเขียนเลยขอบ array ไป บทเรียนนี้กว้างกว่าแค่เรื่อง linearization มาก เมื่อ record ผสมฟิลด์ที่จัดการและไม่จัดการเข้าด้วยกัน ให้กำหนด Default(TRecord) แล้วเลิกคิดว่าฟิลด์ไหน compiler ครอบคลุมให้ และอย่าถือว่าการรันบน Win32 ที่ผ่านฉลุยเป็นหลักฐานว่าการกำหนดค่าเริ่มต้นถูกต้อง
สมาชิก LinearizeOutput และ IsLoadedLinearized ที่กล่าวถึงในบทความนี้มาพร้อมกับ HotPDF Component มาตรฐานสำหรับ Delphi และ C++Builder หน้าผลิตภัณฑ์มีเอกสารอ้างอิง property ฉบับเต็ม รวมถึงกฎการทำงานร่วมกับ cross-reference stream, object stream และการล็อกเวอร์ชัน