ลองใส่รายงานที่สแกนขนาด 80 MB ไว้หลังลิงก์ เปิดในเบราว์เซอร์ แล้วดูว่าเกิดอะไรขึ้น: โปรแกรมดูจะค้างอยู่ที่หน้าต่างว่างเปล่าจนกว่าไบต์ส่วนใหญ่จะโหลดเสร็จ จากนั้นจึงวาดหน้าแรกทั้งหมดในคราวเดียว ลองข้ามไปหน้าที่ 40 แล้วถ้าเป็นไฟล์ที่สร้างมาไม่ดี การดาวน์โหลดทั้งหมดอาจเริ่มต้นใหม่ ส่วนที่น่าหงุดหงิดคือผู้อ่านต้องการแค่หน้าแรกเท่านั้น Linearization คือคำตอบเชิงโครงสร้างสำหรับปัญหานั้น มันจัดเรียง PDF ใหม่เพื่อให้โปรแกรมดูสามารถเรนเดอร์หน้าแรกจากส่วนหัวขนาดเล็กของไฟล์ และดึงส่วนที่เหลือมาเมื่อต้องการ ซึ่งเป็นเหตุผลที่ Adobe ทำการตลาดฟีเจอร์นี้ในชื่อ "Fast Web View"
ทั้งหมดนี้ไม่ใช่รูปแบบไฟล์ใหม่ PDF ที่ถูกทำให้เป็นเชิงเส้น (linearized PDF) คือ PDF ธรรมดาที่โปรแกรมอ่านซึ่งเป็นไปตามมาตรฐานจะเปิดได้โดยไม่ต้องจัดการอะไรเป็นพิเศษ เคล็ดลับทั้งหมดอยู่ที่การเรียงลำดับไบต์ และโครงสร้างพิเศษสองอย่างที่ไฟล์พกพามาด้วย ISO 32000-1 ระบุการจัดเรียงทั้งหมดไว้ในภาคผนวก F (Annex F) และเมื่อคุณเห็นเลย์เอาต์แล้ว พฤติกรรมนี้จะเลิกดูเหมือนเวทมนตร์ แต่จะดูเหมือนการจงใจแลกเปลี่ยนลำดับไฟล์กับความหน่วงในการแสดงผลครั้งแรก (first-paint latency)
การทำ Linearization จัดเรียงอะไรใหม่จริงๆ
PDF ทั่วไปสามารถกระจายออบเจ็กต์ของมันในลำดับเกือบจะแบบใดก็ได้ cross-reference table (ตารางอ้างอิงข้าม) ที่ส่วนท้ายของไฟล์คือสิ่งที่ทำให้มันทำงานได้: โปรแกรมอ่านจะค้นหาไปที่ส่วนท้าย อ่านตัวชี้ startxref โหลด xref และจากนั้นก็สามารถระบุตำแหน่งของทุกออบเจ็กต์ได้จากออฟเซ็ตของมัน การออกแบบนั้นยอดเยี่ยมสำหรับไฟล์ในเครื่อง ซึ่งการค้นหาไปที่ส่วนท้ายไม่เสียเวลาเลย แต่จะแย่มากสำหรับไฟล์ที่สตรีมผ่านเครือข่าย ซึ่งส่วนท้ายคือส่วนที่มาถึงหลังสุดพอดี ในการเรนเดอร์หน้าแรก โปรแกรมอ่านทั่วไปต้องการเพจออบเจ็กต์ (page object), สตรีมเนื้อหา, ฟอนต์ที่มันอ้างอิง และรูปภาพใดๆ ที่มันวาด และในไฟล์ที่ไม่ได้จัดเรียง สิ่งเหล่านี้สามารถอยู่ที่ใดก็ได้ รวมถึงในเมกะไบต์สุดท้ายด้วย
Linearization แก้ไขลำดับเหล่านั้น ออบเจ็กต์ที่จำเป็นในการแสดงหน้าแรกจะถูกรวบรวมไว้ในบล็อกที่ต่อเนื่องกันใกล้กับด้านหน้า ถัดจากส่วนหัวขนาดเล็ก เพื่อให้พวกมันมาถึงตั้งแต่เนิ่นๆ ในสตรีมไบต์ ส่วนที่เหลือทั้งหมด หน้าที่เหลือและทรัพยากรที่พวกมันใช้ร่วมกัน จะตามมาในลำดับที่คาดเดาได้ cross-reference table ชุดที่สองที่สมบูรณ์ยังคงอยู่ที่ส่วนท้ายสำหรับโปรแกรมอ่านที่เพิกเฉยต่อการปรับให้เหมาะสมนี้ แต่ไฟล์ linearized จะวาง cross-reference ของหน้าแรกและพารามิเตอร์ที่โปรแกรมอ่านแบบสตรีมมิงต้องการไว้ด้านหน้า โปรแกรมอ่านไม่ต้องไปถึงส่วนท้ายก่อนจึงจะวาดอะไรได้อีกต่อไป
ชุดออบเจ็กต์หน้าแรกและดิกชันนารีพารามิเตอร์ linearization
ออบเจ็กต์แรกสุดในไฟล์ linearized หลังจากส่วนหัว %PDF คือ linearization parameter dictionary (ดิกชันนารีพารามิเตอร์ linearization) มันคือสิ่งที่โปรแกรมอ่านแบบสตรีมมิงมองหาเพื่อตัดสินใจว่ามีการปรับให้เหมาะสมนี้หรือไม่ และจะใช้มันอย่างไร ดิกชันนารีนี้จะบันทึกความยาวของไฟล์ทั้งหมด, ไบต์ออฟเซ็ตที่ส่วน cross-reference หลักเริ่มต้น, หมายเลขออบเจ็กต์ของหน้าแรก, และตำแหน่งรวมถึงความยาวของ hint stream (สตรีมคำแนะนำ) ที่ตามมา ด้วยตัวเลขเหล่านั้น โปรแกรมอ่านจะรู้ตั้งแต่กิโลไบต์แรกๆ ว่าต้องดึงข้อมูลมามากแค่ไหนเพื่อแสดงหน้าแรก และต้องดูที่ไหนสำหรับดัชนีที่ช่วยให้ข้ามไปยังที่อื่นได้
ภาคผนวก F เข้มงวดมากเกี่ยวกับความหมายของ "หน้าแรก" ในที่นี้ ส่วนของหน้าแรกจะต้องประกอบด้วยตัวเพจออบเจ็กต์เอง, สตรีมเนื้อหาของมัน, และทรัพยากรที่สตรีมเหล่านั้นอ้างอิง เพื่อให้หน้านั้นสามารถทำงานได้ด้วยตัวเองเมื่อส่วนหัวนั้นดาวน์โหลดเสร็จ ทรัพยากรที่ใช้ร่วมกัน (shared resources) เช่น ฟอนต์ที่ใช้ทุกหน้า, โลโก้ที่ซ้ำกันในส่วนหัว จะถูกจัดการเป็นพิเศษ: พวกมันจะปรากฏเร็วพอที่จะให้บริการหน้าแรก แต่จะถูกตั้งค่าแฟล็กเป็นแชร์ เพื่อไม่ให้โปรแกรมอ่านต้องดึงข้อมูลใหม่เมื่อมันไปเรนเดอร์หน้าที่ 30 ในภายหลัง ความแตกต่างระหว่างออบเจ็กต์ส่วนตัวของหน้ากับออบเจ็กต์ที่แชร์ คือสิ่งที่เครื่องมือ "ปรับให้เหมาะสม" ที่ทำขึ้นเองมักจะทำผิดพลาด และการทำผิดพลาดนี้คือสิ่งที่ทำให้เกิดไฟล์ที่อ้างว่า linearized แล้วแต่ยังคงสะดุดอยู่
สตรีมคำแนะนำ: ดัชนีที่ทำให้การข้ามหน้ามีราคาถูก
การแสดงหน้าแรกอย่างรวดเร็วเป็นเพียงครึ่งหนึ่งของคุณค่า อีกครึ่งหนึ่งคือการกระโดดไปยังหน้าใดก็ได้โดยไม่ต้องดาวน์โหลดทุกอย่างที่อยู่ระหว่างนั้น และนั่นคือสิ่งที่ hint streams มอบให้ ไฟล์ linearized จะนำพา page offset hint table (ตารางคำแนะนำออฟเซ็ตของหน้า) และ shared object hint table (ตารางคำแนะนำออบเจ็กต์ที่แชร์) ซึ่งถูกจัดเก็บเป็นสตรีมที่อ้างอิงจากดิกชันนารีพารามิเตอร์ ตารางออฟเซ็ตของหน้าจะบันทึกไว้สำหรับทุกหน้าว่าออบเจ็กต์ของหน้าเหล่านั้นเริ่มต้นที่ไหนในไฟล์และมีความยาวเท่าใด ตารางออบเจ็กต์ที่แชร์ก็ทำเช่นเดียวกันสำหรับทรัพยากรที่ใช้ในหลายๆ หน้า
เมื่อมีตารางเหล่านั้น โปรแกรมอ่านที่ต้องการหน้า 40 จะไม่พาร์สไฟล์ตามลำดับ มันจะปรึกษา hint table เพื่อหาช่วงไบต์ที่หน้า 40 ครอบครอง ขอเซิร์ฟเวอร์เฉพาะช่วงนั้น และเรนเดอร์หน้าเมื่อไบต์เหล่านั้นมาถึง โดยดึงทรัพยากรที่แชร์ใดๆ ที่มันยังไม่มีผ่านกลไกเดียวกัน ในทางปฏิบัติ hint stream คือแผนที่แบบสุ่มเข้าถึง (random-access map) ที่วางทับบนเอกสาร และเป็นเหตุผลว่าทำไมไฟล์ 500 หน้าที่ linearized มาอย่างดีจึงให้ความรู้สึกตอบสนองได้เร็วเมื่อใช้งานผ่านลิงก์ที่ช้า ในขณะที่ไฟล์ที่ไม่ได้ปรับให้เหมาะสมในขนาดเดียวกันจะไม่เป็นเช่นนั้น
ทำไมเซิร์ฟเวอร์ถึงต้องให้ความร่วมมือ
Linearization สมมติว่า transport สามารถส่งชิ้นส่วนใดๆ ของไฟล์ได้ และสมมติฐานนั้นก็คุ้มค่าที่จะตรวจสอบก่อนที่คุณจะโทษรูปแบบไฟล์ว่าให้ผลลัพธ์ที่ไม่ดี กลไกนี้คือ HTTP byte-serving: โปรแกรมอ่านส่งคำขอแบบช่วง (range requests) และเซิร์ฟเวอร์จะตอบกลับด้วยการตอบสนอง 206 Partial Content หากเซิร์ฟเวอร์ไม่ประกาศ Accept-Ranges: bytes หรือถ้า proxy หรือ CDN ด้านหน้ามันยุบคำขอแบบช่วงให้เป็นการถ่ายโอนเต็มรูปแบบ โปรแกรมอ่านจะไม่มีทางดึงข้อมูลหน้า 40 มาเดี่ยวๆ ได้ และต้องกลับไปดาวน์โหลดทั้งไฟล์ โครงสร้างภายใน PDF จะยังคงถูกต้องสมบูรณ์แต่สูญเปล่าไปโดยสิ้นเชิง
นี่คือความล้มเหลวที่มักถูกวินิจฉัยผิดว่าเป็น "linearization ไม่ทำงาน" ตัวไฟล์นั้นปกติ; แต่เส้นทางการส่งมอบไม่ปกติ ก่อนที่คุณจะสร้างเอกสารใหม่ ให้ยืนยันด้วย conditional request ว่าโฮสต์ส่งคืน partial content (เนื้อหาบางส่วน) สำหรับ URL ที่โปรแกรมอ่านเข้าถึงจริงๆ โฮสต์คงที่ (static hosts) จำนวนมากทำเช่นนี้ตามค่าเริ่มต้น และเซิร์ฟเวอร์แอปพลิเคชันรวมถึงแคชเลเยอร์ที่ตั้งค่าผิดจำนวนมากก็ไม่ได้ทำ
การอัปเดตแบบเพิ่มหน่วยทำลาย linearization อย่างเงียบๆ
นี่คือข้อจำกัดที่ทำให้คนที่สร้างไฟล์ linearized อย่างถูกต้องประหลาดใจ และจากนั้นก็สงสัยว่าทำไมการปรับให้เหมาะสมจึงหายไป Linearization พึ่งพาเลย์เอาต์ที่จัดเรียงอย่างระมัดระวังเพียงชุดเดียวพร้อมดัชนีที่ด้านหน้า Incremental update (การอัปเดตแบบเพิ่มหน่วย) ละเมิดสิ่งนั้นโดยการออกแบบ เมื่อเครื่องมือเพิ่มลายเซ็น กรอกช่องฟอร์ม หรือเพิ่มคำอธิบายประกอบผ่านการบันทึกแบบเพิ่มหน่วย (incremental save) มันจะไม่ได้เขียนไฟล์ใหม่ มันจะต่อท้ายออบเจ็กต์ที่เปลี่ยนแปลง, ส่วน cross-reference ใหม่, และ trailer ใหม่เข้าไปที่ส่วนท้าย โดยปล่อยให้ไบต์ดั้งเดิมไม่ถูกแตะต้อง การต่อท้ายนั้นคือประเด็นทั้งหมดของ incremental updates: มันรวดเร็ว และมันรักษารุ่นก่อนหน้าไว้เพื่อตรวจสอบหรือตรวจสอบความถูกต้องของลายเซ็น
ผลข้างเคียงคือตอนนี้ไฟล์มีข้อมูล cross-reference ใหม่ล่าสุดอยู่ที่ส่วนหาง หลังจากบล็อกหน้าแรกที่จัดวางอย่างระมัดระวัง และดิกชันนารีพารามิเตอร์ linearization ที่ด้านหน้าก็จะอธิบายเลย์เอาต์ที่ไม่ตรงกับไฟล์อีกต่อไป โปรแกรมอ่านที่ปฏิบัติตามมาตรฐานจะตรวจจับความไม่ตรงกันนี้ และปฏิบัติต่อเอกสารเหมือนเป็น PDF ธรรมดาที่ไม่ได้ linearized Fast Web View จะหายไป แม้ว่าโครงสร้าง linearized ดั้งเดิมจะยังคงนั่งอยู่ที่ครึ่งแรกของไฟล์ก็ตาม หากคุณต่อท้ายการอัปเดตหลายครั้ง การอัปเดตแต่ละครั้งจะซ้อนรุ่นที่แก้ไขแล้วอีกรุ่นไว้ที่ส่วนท้าย และช่องว่างระหว่างดัชนีด้านหน้าที่ล้าสมัยกับสถานะจริงก็จะกว้างขึ้น
หากเวิร์กโฟลว์ของคุณต้องการทั้งการแก้ไขและ Fast Web View กฎนั้นจะเป็นไปตามโครงสร้างโดยตรง: แก้ไขแบบเพิ่มหน่วยในขณะที่เอกสารมีการเปลี่ยนแปลง จากนั้นทำ linearization ใหม่อีกครั้งเมื่อเสร็จสิ้น การเขียนใหม่ทั้งหมด (full rewrite) คือสิ่งที่จะกู้คืนเลย์เอาต์ ในบริบทของ HotPDF หมายความว่าการแก้ไขที่กำลังดำเนินอยู่จะผ่าน BeginIncrementalUpdate และ SaveIncrementalUpdate ซึ่งจะต่อท้ายส่วนต่าง (delta) ในขณะที่ขั้นตอนสุดท้ายจะโหลดเอกสารทั้งหมดและสร้างอนุกรม (serialize) ขึ้นมาใหม่ด้วย LoadFromFile ตามด้วย SaveLoadedDocument ซึ่งจะทิ้งรุ่นเก่าที่สะสมไว้และปล่อยเลย์เอาต์ที่สะอาดออกมาหนึ่งชุด การแลกเปลี่ยนเดียวกันนี้จะปรากฏขึ้นพร้อมกับออบเจ็กต์สตรีม (object streams): การเปิดใช้งาน UseObjectStreams ร่วมกับ UseXRefStream จะบีบอัด cross-reference และแพ็กออบเจ็กต์อย่างแน่นหนา ซึ่งช่วยเรื่องขนาดไฟล์ แต่ก็เช่นเดียวกับตัวเลือกโครงสร้างอื่นๆ มันจะต้องถูกนำไปใช้ในระหว่างการเขียนใหม่ครั้งสุดท้ายนั้น แทนที่จะยึดติดกับรุ่นที่แก้ไขแล้วที่ถูกต่อท้ายเข้ามา
// In-flight edits: append a delta, keep prior revisions intact.
// This leaves the file NOT linearized.
Pdf.BeginIncrementalUpdate('report.pdf');
Pdf.AddPage;
Pdf.CurrentPage.TextOut(72, 760, 0, 'Addendum');
Pdf.SaveIncrementalUpdate('report.pdf');
// Finishing step: full re-serialization produces one clean layout,
// dropping the stacked revisions. Re-run your linearizer on the output.
Pdf.LoadFromFile('report.pdf');
Pdf.SaveLoadedDocument('report-final.pdf');
HotPDF ไม่ได้เปิดเผยกิจวัตร "linearize" แบบเรียกครั้งเดียว ดังนั้นรูปแบบในทางปฏิบัติคือการสร้างไฟล์ที่สะอาดและเขียนใหม่ทั้งหมด แล้วเรียกใช้เครื่องมือเพิ่มประสิทธิภาพเฉพาะสำหรับไฟล์นั้น เครื่องมือบรรทัดคำสั่ง (Command-line tools) จะจัดการการจัดเรียงใหม่โดยตรง qpdf จะเขียนไฟล์ใหม่ให้อยู่ในรูปแบบ linearized ด้วยแฟล็กเดียว:
qpdf --linearize report-final.pdf report-web.pdf
วิธีบอกได้ว่าไฟล์ผ่านการ linearized หรือไม่
อย่าเชื่อชื่อไฟล์หรือเครื่องมือที่อ้างว่าสร้างมันขึ้นมา; ให้ตรวจสอบที่ไบต์ การตรวจสอบที่ตรงที่สุดคือส่วนหัวของไฟล์: เปิดมันขึ้นมาและมองหาดิกชันนารีพารามิเตอร์ linearization เป็นออบเจ็กต์แรกหลังจากส่วนหัว ซึ่งจะนำพาคีย์ /Linearized มาด้วย ทางลัดสำหรับโปรแกรมอ่านคือกล่องโต้ตอบคุณสมบัติเอกสาร (Document Properties) ของ Acrobat ซึ่งจะรายงาน "Fast Web View: Yes" ก็ต่อเมื่อมีโครงสร้างนั้นอยู่จริงและเป็นปัจจุบันเท่านั้น
สำหรับการตรวจสอบด้วยสคริปต์ qpdf จะรายงานทั้งการมีอยู่และความสมบูรณ์ของโครงสร้าง ซึ่งสำคัญมากเพราะไฟล์สามารถนำพาดิกชันนารี linearization ที่ไม่ได้สะท้อนถึงเลย์เอาต์ของมันอีกต่อไป ซึ่งเป็นสถานะที่การอัปเดตแบบเพิ่มหน่วยทิ้งไว้อย่างพอดิบพอดี:
# Reports "File is linearized" and validates hint tables against the layout
qpdf --check report-web.pdf
# Dumps the linearization parameters and hint data in detail
qpdf --show-linearization report-web.pdf
ขั้นตอนการตรวจสอบความถูกต้อง (validation) คือขั้นตอนที่คุ้มค่า การส่งผ่านที่เพียงแค่ยืนยันว่าดิกชันนารีมีอยู่ จะยอมรับไฟล์ที่ดัชนีชี้ไปที่ออฟเซ็ตที่ผิดได้อย่างมีความสุข; การตรวจสอบที่เปรียบเทียบตารางคำแนะนำกับตำแหน่งของออบเจ็กต์จริงต่างหากที่จะบอกคุณได้ว่า การปรับให้เหมาะสมนี้จะรับมือกับคำขอแบบช่วงของโปรแกรมอ่านจริงได้หรือไม่
Linearization ยังคงคุ้มค่าที่จะนำไปใช้กับเอกสารขนาดใหญ่ใดๆ ที่ให้บริการผ่านเว็บ โดยเฉพาะอย่างยิ่งกับผู้อ่านบนมือถือที่การเชื่อมต่อไม่เสถียร และมันใช้ขนาดไฟล์เพียงไม่กี่เปอร์เซ็นต์สำหรับดัชนีที่โหลดไว้ล่วงหน้า (front-loaded index) สองสิ่งที่ต้องจำไว้เสมอคือ โครงสร้างภายใน PDF และ byte-serving ภายนอกจะต้องถูกต้องทั้งคู่ และการแก้ไขใดๆ หลังจากนั้นจะเป็นการยกเลิกการปรับให้เหมาะสมจนกว่าคุณจะเขียนไฟล์ใหม่ ให้ถือว่าการ relinearization เป็นขั้นตอนสุดท้ายในไปป์ไลน์ หลังจากที่การเปลี่ยนแปลงอื่นๆ ทั้งหมดเรียบร้อยแล้ว พฤติกรรมของ cross-reference, object-stream และ incremental-update ที่อธิบายไว้ที่นี่ เป็นส่วนหนึ่งของโมเดลโครงสร้างที่ HotPDF Component สำหรับ Delphi และ C++Builder นำไปใช้; สำหรับเบื้องหลังของ file-layout ในภาพกว้าง ดูที่ โครงสร้างไฟล์ PDF เป็นอย่างไร และสำหรับขั้นตอนการทำงาน incremental-update และไฟล์ขนาดใหญ่ในโค้ด ดูที่ การประมวลผล PDF ขนาดใหญ่จาก Delphi