คลังเอกสารสแกนอาจมีขนาดใหญ่หลายกิกะไบต์ในไฟล์ PDF ไฟล์เดียว โปรแกรมแสดงผลที่เปิดไฟล์ลักษณะนี้โดยปกติมักต้องการแสดงผลเพียงหน้าเดียว เช่น หน้าสารบัญ หรือหน้าที่ผู้ใช้กดข้ามผ่านบุ๊กมาร์ก (bookmark) การอ่านเนื้อหาทั้งไฟล์เข้าไปในหน่วยความจำเพื่อแสดงผลกระดาษเพียงสองหน้าถือเป็นการสิ้นเปลืองในทุก ๆ มิติ: ทั้งสูญเสียพื้นที่แอดเดรส (address space) ทำให้ผู้ใช้ต้องนั่งรอขั้นตอนการอ่านข้อมูลช่วงแรกเป็นเวลานาน และสำหรับระบบประมวลผล Delphi แบบ 32 บิต ขั้นตอนการทำงานอาจล้มเหลวไปเลยก่อนที่จะมีหน้ากระดาษแม้แต่หน้าเดียวแสดงผลขึ้นมา PDFium ถูกพัฒนาขึ้นมาเพื่อรับมือกับโจทย์นี้โดยเฉพาะ มันสามารถโหลดเอกสารผ่านคอลแบ็กที่จะขอข้อมูลช่วงไบต์ที่ต้องการจริง ๆ เมื่อจำเป็นต้องใช้งาน และไม่มีการเรียกร้องต้องการอ่านข้อมูลทั้งหมดของไฟล์พร้อมกัน
ตัวคอมโพเนนต์จะนำเสนอวิธีการนี้ผ่านตัวแปลงสตรีม (stream adapter) คุณสามารถส่งมอบอ็อบเจกต์ TStream ใด ๆ ให้มัน และ PDFium จะดึงข้อมูลบล็อกต่าง ๆ จากสตรีมนั้นตามความต้องการจริง ตัวไฟล์สามารถจัดเก็บไว้บนดิสก์ ในฟิลด์ blob ของฐานข้อมูล หรือยู่ภายใต้อ็อบเจกต์ที่สืบทอดจาก TStream อื่น ๆ โดยไม่มีส่วนใดของไฟล์ถูกคัดลอกเข้าไปในหน่วยความจำตั้งแต่ขั้นตอนเริ่มต้น
PDFium ร้องขอข้อมูลไบต์อย่างไร
API ภาษา C ของ PDFium จะทำการโหลดเอกสารจากอ็อบเจกต์ที่ผู้เรียกป้อนเข้ามาซึ่งอธิบายโดยโครงสร้างข้อมูล FPDF_FILEACCESS โครงสร้างนี้มีสามส่วนสำคัญได้แก่: ฟิลด์ระบุขนาดความยาว คอลแบ็กการอ่านค่า และพารามิเตอร์ผู้ใช้ที่เป็นความลับ (opaque parameter) จุดเข้าใช้งานข้อมูลที่เรียกใช้อ็อบเจกต์นี้คือฟังก์ชัน FPDF_LoadCustomDocument เมื่อ PDFium ถือครองโครงสร้างดังกล่าวแล้ว มันจะเริ่มต้นวิเคราะห์ข้อมูลส่วนท้าย ค้นหาตำแหน่งตารางการอ้างอิงไขว้ และหลังจากนั้นจะเลือกอ่านเฉพาะข้อมูลที่กระบวนการนั้นร้องขอ การเปิดเอกสารจะเข้าถึงข้อมูลส่วนปลายสุดของไฟล์และกลุ่มอ็อบเจกต์แคตตาล็อกเพียงเล็กน้อย การแสดงผลหน้า 400 จะเข้าไปอ่านสตรีมเนื้อหาและทรัพยากรเฉพาะสำหรับหน้าดังกล่าวเท่านั้นโดยไม่มีการแตะต้องข้อมูลส่วนอื่นเลย
นี่คือความแตกต่างระหว่างการโหลดแบบเก็บข้อมูลสำรอง (buffered load) และการโหลดแบบสตรีม (streaming load) การโหลดแบบเก็บข้อมูลสำรองจะอ่านไฟล์ตั้งแต่ต้นจนจบก่อนที่ PDFium จะเริ่มมองเห็นข้อมูลไบต์แรก แต่การโหลดแบบสตรีมจะสลับทิศทางกัน: PDFium จะเป็นผู้ควบคุมกระบวนการอ่านข้อมูล และข้อมูลไบต์ที่ไม่เคยถูกเรียกใช้ก็จะไม่เคยถูกเปิดอ่านเลย สำหรับไฟล์ขนาดหลายกิกะไบต์ที่เปิดอ่านทีละหน้า นี่คือตัวแปรที่แยกความแตกต่างระหว่างระบบโหลดที่ไม่สามารถใช้งานได้จริงกับระบบโหลดที่แสดงผลได้ในทันที
ตัวแปลงสตรีม
ตัวแปลงที่เชื่อมต่อ TStream ของ Delphi เข้ากับ FPDF_FILEACCESS คือ TPdfStreamAdapter ตัวสร้าง (constructor) ของมันจะรับค่าสตรีมและค่าแฟล็กความเป็นเจ้าของ อ่านค่าขนาดสตรีมเก็บไว้ครั้งหนึ่ง เติมข้อมูลลงในเรกคอร์ด FPDF_FILEACCESS และเชื่อมโยงคอลแบ็กการอ่านค่า เมื่อ PDFium เรียกกลับมาในภายหลังพร้อมค่าออฟเซตและขนาดข้อมูล ตัวแปลงจะปรับตำแหน่งค้นหา (seek) สตรีมไปยังออฟเซตนั้นและคัดลอกข้อมูลในช่วงดังกล่าวลงในพื้นที่จัดเก็บข้อมูลที่ PDFium เตรียมไว้โดยละเอียด
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
ตัวแฟล็กสิทธิ์ความเป็นเจ้าจะเป็นตัวตัดสินว่าใครจะเป็นคนเคลียร์ค่าสตรีม หากส่งค่า False ตัวผู้เรียกจะยังคงถือครองสตรีมและมีหน้าที่ดูแลให้มันยังมีตัวตนอยู่ตลอดอายุการทำงานของเอกสาร หากส่งค่า True ตัวแปลงจะรับหน้าที่ดูแลแทน และสั่งปลดปล่อยสตรีมเมื่อปิดเอกสาร ไม่ว่าจะเลือกแบบใด สตรีมจะต้องอยู่ยืนยาวกว่าขั้นตอนการอ่านค่าใด ๆ ที่ PDFium จะรัน เนื่องจาก PDFium จะถือครองตัวชี้ข้อมูล FPDF_FILEACCESS เอาไว้และอาจเรียกกลับมาในตำแหน่งใด ๆ ก็ได้ขณะที่เอกสารยังเปิดอยู่ ไม่ใช่เฉพาะช่วงการโหลดแรกเท่านั้น
ทำไมคอลแบ็กจึงเป็นฟังก์ชันสแตติก
คอลแบ็กการอ่านค่าที่ PDFium จัดเก็บไว้ใน m_GetBlock คือตัวชี้ฟังก์ชันภาษา C ธรรมดาที่มีข้อตกลงการเรียกแบบ cdecl ซึ่งจะไม่มีทางเรียกใช้งานเมธอดของ Delphi ได้โดยตรง เนื่องจากตัวเมธอดจำเป็นต้องส่งผ่านอาร์กิวเมนต์แฝง Self ซึ่งผู้เรียกใช้ภาษา C ไม่รู้จักและไม่มีทางส่งเข้ามาให้ ตัวแปลงจึงต้องประกาศคอลแบ็กให้เป็น class function ที่ระบุข้อตกลง cdecl; static ซึ่งจะถูกคอมไพล์ให้เป็นฟังก์ชันอิสระที่มีเค้าโครงเฟรมภาษา C ตามที่ PDFium ต้องการและไม่มีพารามิเตอร์แฝง Self
วิธีดังกล่าวสามารถแก้ปัญหาข้อตกลงการเรียกได้ แต่ก็สร้างคำถามข้อที่สองตามมา: เมื่อไม่มีตัวแปร Self คอลแบ็กจะสามารถเข้าถึงสตรีมที่มันต้องทำหน้าที่อ่านได้อย่างไร คำตอบคือพารามิเตอร์ผู้ใช้ที่เป็นความลับ เมื่อตัวแปลงสร้างเรกคอร์ดขึ้นมา มันจะเก็บตัวชี้อินสแตนซ์ของมันไว้ใน m_Param PDFium จะส่งตัวชี้ตัวเดิมนี้กลับมาในอาร์กิวเมนต์ตัวแรกในทุกครั้งที่มีการเรียกใช้งานคอลแบ็ก ฟังก์ชันสแตติกจะแปลงค่าตัวชี้นี้กลับเป็น TPdfStreamAdapter และสั่งให้เริ่มอ่านข้อมูลจากสตรีมของอินสแตนซ์นั้น นี่คือเทคนิคการส่งต่อข้อมูลแบบแทรมโพลีน (trampoline) ทั่วไปเพื่อส่งบริบทของอ็อบเจกต์ข้ามขอบเขตภาษา C ที่ไม่มีระบบจัดการอ็อบเจกต์
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
เพดานขนาด 4 GiB และทำไมจึงต้องมีตัวป้องกัน
ฟิลด์ขนาดความยาว m_FileLen ใน FPDF_FILEACCESS เป็นประเภทข้อมูลไม่มีเครื่องหมายขนาด 32 บิต ขนาดความยาวสูงสุดที่มันสามารถแสดงได้คือขาดไปหนึ่งไบต์จะครบ 4 GiB แต่อ็อบเจกต์ TStream จะรายงานขนาดข้อมูลในรูปแบบ Int64 ทำให้สตรีมสามารถระบุขนาดความยาวได้มากกว่าขีดจำกัดที่ฟิลด์ดังกล่าวจะเก็บได้มาก ทันทีที่ขนาดสตรีมใหญ่เกินเพดานสูงสุด จะไม่มีทางบอกขนาดไฟล์ที่แท้จริงให้ PDFium ทราบได้เลย
แนวทางตอบสนองที่ผิดคือการกำหนดค่าขนาดข้อมูลเข้าไปดื้อ ๆ และปล่อยให้เกิดปัญหาน้ำล้นข้อมูล การตัดทอนขนาด 5 GiB ลงในฟิลด์ข้อมูลขนาด 32 บิตจะทำให้ได้ตัวเลขที่มีขนาดเล็กลงแต่ดูสมเหตุสมผล และทำให้ PDFium เข้าใจว่าไฟล์สิ้นสุดที่ตำแหน่งความยาวประมาณหนึ่งกิกะไบต์ ข้อมูลส่วนท้ายและตารางการอ้างอิงไขว้จริง ๆ จะอยู่ที่ส่วนท้ายสุดของไฟล์ ซึ่งอยู่ไกลออกไปพ้นค่าความยาวที่ถูกตัดทอนมาก ส่งผลให้ขั้นตอนวิเคราะห์ล้มเหลวด้วยสาเหตุที่ดูเหมือนจะไม่เกี่ยวข้องกับสาเหตุที่แท้จริงเลย คุณอาจจะวุ่นวายกับการตรวจแก้จุดบกพร่องข้อผิดพลาดของการอ้างอิงไขว้ในไฟล์ที่สมบูรณ์ดี โดยไม่มีคำแนะนำบอกเลยว่าจำนวนเต็มเกิดปัญหาล้นตั้งแต่สองลำดับชั้นข้างบน
ตัวแปลงจะทำการปฏิเสธข้อมูลนำเข้าแทน ตัวสร้างจะเปรียบเทียบขนาดสตรีมเทียบกับค่าสูงสุดของ High(FPDF_DWORD) และโยนข้อยกเว้น EPdfError ทันทีที่พบว่าสตรีมมีขนาดใหญ่เกินกว่าจะระบุได้ ข้อผิดพลาดที่แจ้งตรงไปตรงมาในจุดสร้างจะระบุสาเหตุปัญหาที่แท้จริงได้ทันที การตัดทอนข้อมูลอย่างเงียบ ๆ จะบดบังสาเหตุแท้จริงไว้เบื้องหลังข้อผิดพลาดแบบอื่นที่คุณจะตามหาอีกนาน ขีดจำกัด 4 GiB เป็นข้อจำกัดทางกายภาพที่แท้จริงของพาธโหลดนี้ และการรายงานผลอย่างตรงไปตรงมาคือแนวทางที่ดีที่สุด แทนที่จะปล่อยให้โค้ดรันคำสั่งตัวเลขที่สามารถคอมไพล์ผ่านเฉย ๆ
ความล้มเหลวจะต้องไม่ข้ามผ่านขอบเขตภาษาเป็นอันขาด
การอ่านข้อมูลสามารถทำงานล้มเหลวได้ ตัวสตรีมอาจเป็นอ็อบเจกต์ที่รับส่งข้อมูลผ่านเครือข่ายแล้วเกิดหมดเวลา ตัวจัดการ blob ถูกปิดทำงานระหว่างประมวลผล หรือตัวไฟล์ถูกตัดทอนขนาดหลังจากเอกสารเปิดใช้งานแล้ว ข้อตกลงของ PDFium สำหรับคอลแบ็กการอ่านค่าคือการคืนรหัสผลลัพธ์: ค่าที่ไม่ใช่ศูนย์แทนความสำเร็จ และค่าศูนย์แทนความล้มเหลว โครงสร้างข้อมูลเป็นเฟรมภาษา C และไม่มีกลไกตรวจจับหรือส่งข้อยกเว้นของ Pascal ออกไป
นี่เป็นสาเหตุที่ทำให้ตัวเชื่อมต่อสตรีมครอบขั้นตอนค้นหาตำแหน่งและการอ่านไว้ในบล็อก try/except เพื่อปิดบังข้อยกเว้นไว้และส่งคืนค่าศูนย์กลับไปแทน หากข้อยกเว้นของ Delphi ได้รับอนุญาตให้วิ่งทะลุคอลแบ็กออกไป มันจะคลายสแต็กข้อมูลผ่านเฟรมสแต็ก cdecl ของ PDFium ซึ่งไม่ได้ออกแบบมารองรับข้อยกเว้นของ Pascal ผลลัพธ์สุดท้ายอาจรุนแรงถึงขั้นโปรแกรมปิดตัวลงทันทีในส่วนวิเคราะห์ PDF ลึก ๆ โดยไม่มีสแต็กประวัติให้สืบค้น การคืนค่าศูนย์กลับไปจะช่วยควบคุมความผิดพลาดให้อยู่ในข้อกำหนด PDFium จะมองเห็นความล้มเหลวในการอ่านบล็อกข้อมูล ยกเลิกการทำงานอย่างสะอาดสะอ้าน และส่งคำสั่ง FPDF_LoadCustomDocument รายงานว่าไม่สามารถโหลดเอกสารได้ ซึ่งคอมโพเนนต์จะนำมารายงานเป็น EPdfError ในฝั่งของ Pascal ที่มันสังกัดอยู่
การเปิดใช้งานเอกสารด้วยวิธีนี้
เมธอดคอมโพเนนต์ที่ควบคุมการทำงานของพาธสตรีมมิ่งคือ LoadCustomDocument ซึ่งถูกประกาศแยกเฉพาะออกมาแทนที่จะเป็นเมธอดแบบโอเวอร์โหลด (overload) ทั่วไปของ LoadDocument เพื่อไม่ให้การส่งผ่านค่า TMemoryStream วิ่งเข้าไปในพาธแบบมีข้อมูลสำรองโดยไม่ได้ตั้งใจ มันจะเริ่มสร้างตัวแปลง เรียกใช้ฟังก์ชัน FPDF_LoadCustomDocument และดูแลให้ตัวแปลงยังมีชีวิตตลอดการเปิดใช้งานเอกสารนั้น
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
คำสั่งเดียวกันนี้ใช้งานได้กับ TMemoryStream สตรีมแบบ blob จากชุดข้อมูลฐานข้อมูล หรืออ็อบเจกต์ที่สืบทอดจาก TStream ตัวอื่น ๆ การโหลดตามความต้องการจะแสดงศักยภาพของมันได้อย่างคุ้มค่าเมื่อไฟล์มีขนาดใหญ่มากและมีเพียงบางส่วนของไฟล์ที่จะถูกอ่าน: เช่น โปรแกรมแสดงคลังเอกสาร ตัวสร้างภาพตัวอย่างขนาดเล็กที่อ่านเพียงไม่กี่หน้ากระดาษ หรือการทำดัชนีสืบค้นที่อ่านข้อมูลทีละหน้า เมื่อไฟล์มีขนาดเล็กหรือเมื่อคุณต้องการอ่านข้อมูลทั้งหมดของไฟล์อยู่แล้ว การโหลดแบบสำรองข้อมูลจะเรียบง่ายกว่าและกลไกการสตรีมจะไม่มีความจำเป็น ปัจจัยตัดสินจึงอยู่ที่สัดส่วนข้อมูลที่คุณจะใช้งานจริงเทียบกับสัดส่วนข้อมูลที่จัดเก็บอยู่ในไฟล์
เมื่อหน้ากระดาษสตรีมเข้ามาตามความต้องการแล้ว สิ่งที่ต้องคำนึงถัดไปคือการทำให้การแสดงผลสามารถตอบสนองต่อผู้ใช้ได้อย่างรวดเร็วในขั้นตอนการซูมและเลื่อนหน้ากระดาษ ซึ่งศึกษาต่อได้จาก บันทึกของเราเกี่ยวกับแคชการแสดงผลและประสิทธิภาพการซูม และเมื่อเอกสารสตรีมเข้ามาเป็นสิ่งที่ต้องการเพียงให้แสดงผลแต่ไม่อนุญาตให้ผู้ใช้ส่งออกหรือดัดแปลง เทคนิคใน คู่มือการพรีวิวไฟล์ PDF อย่างปลอดภัย จะจับคู่กับการใช้งานร่วมกับพาธโหลดแบบนี้ได้อย่างลงตัว ทั้งคู่พัฒนาขึ้นบนระบบการโหลดสตรีมที่อธิบายไว้ที่นี่ ซึ่งมีมาพร้อมเป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ C++Builder ร่วมกับ API การแสดงผล การแยกข้อความ และการจัดการคำอธิบายประกอบที่ระบุในบล็อกนี้