Technical Article

การเรนเดอร์ PDF แบบก้าวหน้าที่ยกเลิกได้ใน Delphi (PDFium)

หน้า PDF ส่วนใหญ่จะทำแรสเตอร์ไลซ์ในเวลาไม่กี่มิลลิวินาทีและคุณไม่เคยต้องคิดถึงมันเลย จากนั้นผู้ใช้เปิดแบบวิศวกรรมขนาด A1 หน้ากระดาษที่อัดแน่นไปด้วยลายเส้นเวกเตอร์นับหมื่นเส้น หรือโปสเตอร์ที่เต็มไปด้วยกลุ่มความโปร่งใส (transparency groups) และซอฟต์มาสก์ (soft masks) และการเรียกใช้เพียงครั้งเดียวเพื่อวาดภาพนั้นก็ใช้เวลาถึงสองหรือสามวินาที หากการเรียกใช้นั้นรันบนเธรด UI หน้าต่างจะหยุดวาดใหม่ แถบชื่อเรื่องจะเป็นสีเทา และระบบปฏิบัติการจะเสนอให้ฆ่าแอปพลิเคชัน งานนั้นเป็นงานที่ถูกต้องตามกฎหมาย หน้ากระดาษนั้นต้องการเวลานานขนาดนั้นจริงๆ ข้อบกพร่องคือการเรนเดอร์นั้นเป็นการเรียกแบบบล็อกที่แบ่งแยกไม่ได้ (indivisible blocking call) โดยไม่มีทางที่จะขึ้นมาหายใจและไม่มีทางที่จะหยุดได้

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

API การเรนเดอร์แบบก้าวหน้าที่ PDFium มีให้อยู่แล้ว

PDFium ได้คาดการณ์ล่วงหน้าถึงปัญหาครึ่งหนึ่งที่ทำให้ระบบหยุดนิ่ง นอกเหนือจาก FPDF_RenderPageBitmap แบบครั้งเดียวจบแล้ว มันยังเปิดเผยตัวแปรแบบก้าวหน้าที่แบ่งหน้ากระดาษออกเป็นชิ้นงานต่างๆ (chunks of work) คุณเรียก FPDF_RenderPageBitmap_Start หนึ่งครั้งเพื่อตั้งค่าการเรนเดอร์กับบิตแมปปลายทาง จากนั้นจึงเรียก FPDF_RenderPage_Continue ซ้ำๆ แต่ละ Continue จะทำแรสเตอร์ไลซ์ในสไลซ์ที่มีขอบเขตและส่งคืนสถานะ FPDF_RENDER_TOBECONTINUED หมายความว่ายังมีงานให้ทำอีก FPDF_RENDER_DONE หมายความว่าหน้ากระดาษเสร็จแล้ว และ FPDF_RENDER_FAILED หมายความว่ามันหยุดลงเนื่องจากเกิดข้อผิดพลาด เมื่อลูปสิ้นสุด คุณก็เรียก FPDF_RenderPage_Close เพื่อปล่อยสถานะก้าวหน้าต่อหน้า (per-page progressive state) เนื่องจากการควบคุมจะกลับไปที่โค้ดของคุณในระหว่างสไลซ์ คุณจึงสามารถสูบข้อความ อัปเดตตัวบ่งชี้ความคืบหน้า หรือตรวจสอบว่างานนั้นยังเป็นที่ต้องการอยู่หรือไม่

กลไกที่ PDFium มอบให้สำหรับการตัดสินใจว่าจะยอมทำตามเมื่อใดนั้นคือ struct ของโคลแบ็กที่มีชื่อว่า IFSDK_PAUSE คุณส่งมอบมันให้กับ Start และ Continue ทุกตัว หลังจากแต่ละชิ้นส่วน (chunk) PDFium จะเรียกฟังก์ชันพอยน์เตอร์ NeedToPauseNow ของมัน และถ้าสิ่งนั้นส่งคืนค่าที่ไม่ใช่ศูนย์ Continue ปัจจุบันก็จะหยุดก่อนกำหนดและส่งมอบการควบคุมกลับไปพร้อมกับ FPDF_RENDER_TOBECONTINUED โครงสร้าง (struct) ยังมีฟิลด์ version ซึ่งต้องตั้งค่าเป็น 1 และพอยน์เตอร์แบบอิสระ user ซึ่ง PDFium จะไม่เคยแตะต้องมันและส่งผ่านมันไปโดยไม่ถูกแตะต้อง พอยน์เตอร์ที่ไม่ถูกแตะต้องนั้นคือบานพับทั้งหมดของการออกแบบที่จะตามมา

การเปลี่ยนจุดประสงค์การหยุดชั่วคราวให้เป็นการยกเลิก

ความตั้งใจดั้งเดิมของ NeedToPauseNow คือการแบ่งเวลา (time-slicing) ส่งคืนค่าที่ไม่ใช่ศูนย์เมื่องบประมาณเฟรมของคุณถูกใช้หมดไป ส่งคืนศูนย์เพื่อให้การเรนเดอร์ดำเนินต่อไป แล้ว PDFium จะหยุดชั่วคราวเพื่อให้คุณสามารถทำอย่างอื่นได้ก่อนที่จะกลับมาทำการเรนเดอร์แบบเดียวกัน PDFium Component นำสัญญาณเดียวกันนั้นกลับมาใช้ใหม่สำหรับคำกริยาที่แตกต่างออกไป แทนที่จะตอบว่า "ฉันควรหยุดชั่วคราวและให้คุณทำต่อหรือไม่" โคลแบ็กจะตอบว่า "งานนี้ถูกยกเลิกแล้วหรือยัง" ทั้งสองอย่างนี้แมปเข้าหากันอย่างสะอาดหมดจดเนื่องจากสิ่งที่ลูปทำเมื่อมันเห็นแฟล็ก การหยุดชั่วคราวอย่างแท้จริงจะคาดหวังว่าจะมี Continue ในภายหลัง; การยกเลิกไม่คาดหวังเช่นนั้น เมื่อลูปที่เรียก (calling loop) สังเกตเห็นว่าโทเค็นถูกยกเลิก มันจะปิดบริบท (context) การเรนเดอร์และไม่เคยเรียก Continue อีกเลย ดังนั้นค่าส่งคืนที่ไม่ใช่ศูนย์เดียวกันกับที่ PDFium อ่านเป็น "หยุดชิ้นส่วนนี้" ก็จะกลายเป็น "หยุดแบบถาวร" ในทางปฏิบัติ

การยกเลิกถูกแสดงออกผ่านอินเทอร์เฟซ (interface) คือ IPdfCancellationToken ซึ่งพร็อพเพอร์ตี้ IsCancelled ของมันจะพลิกจาก false เป็น true เมื่อส่วนอื่นๆ ของโปรแกรมขอให้การเรนเดอร์หยุดทำงาน สะพานเชื่อมระหว่างอินเทอร์เฟซ Pascal นั้นกับโคลแบ็ก C ของ PDFium คือพอยน์เตอร์เดียว การอ้างอิงอินเทอร์เฟซของโทเค็นจะถูกเขียนลงใน IFSDK_PAUSE.user และโคลแบ็ก cdecl แบบคงที่ (static) จะอ่านมันกลับออกมาและสืบค้นมัน นี่คือปัญหาคลาสสิกของการปล่อยให้ไลบรารี C โคลแบ็กกลับเข้าไปใน Pascal: โคลแบ็กต้องเป็นฟังก์ชันธรรมดาที่มีข้อกำหนดการเรียกแบบ C ไม่ใช่เมธอด เพราะ PDFium จะเก็บและเรียกใช้ฟังก์ชันพอยน์เตอร์เปล่าๆ ที่ไม่รู้อะไรเลยเกี่ยวกับออบเจ็กต์ของ Pascal หรือ Self

type
  TPdfProgressivePause = record
    Pause: IFSDK_PAUSE;            // PDFium reads this; .user holds the token
    Token: IPdfCancellationToken; // strong ref keeps the token alive
  end;

function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
  Token: IPdfCancellationToken;
begin
  Result := 0;
  if (pThis = nil) or (pThis^.user = nil) then
    Exit;
  Token := IPdfCancellationToken(pThis^.user);
  if Token.IsCancelled then
    Result := 1; // non-zero: PDFium stops this chunk
end;

โคลแบ็กกู้คืนโทเค็นโดยการหล่อ (casting) pThis^.user กลับไปเป็นประเภทอินเทอร์เฟซและอ่าน IsCancelled ไม่มีสิ่งใดในนั้นที่จัดสรรทรัพยากร (allocates) ล็อก หรือบล็อก ซึ่งมีความสำคัญเนื่องจาก PDFium จะเรียกมันในเธรดการเรนเดอร์หลังจากแต่ละชิ้นส่วน (chunk) และงานใดๆ ที่ทำขึ้นที่นี่จะถูกบวกเข้าไปกับต้นทุนของการเรนเดอร์เอง การป้องกันไม่ให้มี struct ว่างเปล่าหรือฟิลด์ user ว่างเปล่า หมายความว่าฟังก์ชันเดียวกันนี้ปลอดภัยที่จะติดตั้ง แม้กระทั่งกับการเรนเดอร์ที่ไม่เคยได้รับโทเค็นจริงเลยก็ตาม

การรักษาโทเค็นให้คงอยู่ตลอดลูป

การหล่ออินเทอร์เฟซพอยน์เตอร์ผ่าน Pointer ดิบๆ แล้วหล่อกลับมา คือจุดที่บั๊กด้านอายุการใช้งาน (lifetime bugs) ถือกำเนิดขึ้น IInterface ใน Delphi มีการนับการอ้างอิง (reference counted) และการนับจะขยับก็ต่อเมื่อคอมไพเลอร์สามารถมองเห็นตัวแปรแบบอินเทอร์เฟซกำลังถูกกำหนดค่า (assigned) การจัดเก็บโทเค็นเป็นพอยน์เตอร์เปล่าๆ ภายใน IFSDK_PAUSE.user เพียงอย่างเดียว จะซ่อนมันจากตัวนับการอ้างอิงโดยสิ้นเชิง หากการอ้างอิงอื่นๆ เพียงอย่างเดียวที่มีต่อโทเค็นนั้นหลุดออกนอกขอบเขตในขณะที่ลูป Continue ยังคงรันอยู่ ออบเจ็กต์ก็จะถูกปลดปล่อยให้เป็นอิสระไปจากใต้เท้าโคลแบ็ก และชิ้นส่วนถัดไปก็จะถอดรหัส (dereference) พอยน์เตอร์ที่ห้อยต่องแต่ง (dangling pointer)

นั่นคือเหตุผลว่าทำไมตัวบรรยาย (descriptor) จึงเป็นบันทึก (record) ที่เก็บของสองสิ่ง ไม่ใช่สิ่งเดียว ฟิลด์ Pause คือ struct ที่ PDFium อ่าน ฟิลด์ Token เป็นการอ้างอิงแบบอินเทอร์เฟซที่แท้จริงที่คอมไพเลอร์จะนับ และมันก็ไม่มีเหตุผลอื่นใดนอกจากการตรึง (pin) โคลแบ็กไว้ในหน่วยความจำตราบเท่าที่บันทึกนั้นยังมีชีวิตอยู่ บันทึกคือตัวแปรในตัว (local variable) บนสแต็กของกิจวัตรการเรนเดอร์ ดังนั้นมันจึงยังคงใช้ได้ตลอดระยะเวลาที่ลูปดำเนินอยู่และจะถูกรื้อถอนเมื่อกิจวัตรนั้นสิ้นสุดลงเท่านั้น พอยน์เตอร์เปล่าๆ ใน user และการอ้างอิงที่มีการนับใน Token คือชื่อของออบเจ็กต์เดียวกัน; อันหนึ่งคือสิ่งที่ PDFium สามารถอ่านได้ อีกอันหนึ่งคือสิ่งที่ป้องกันไม่ให้ออบเจ็กต์นั้นถูกเก็บรวบรวม

var
  Pause: TPdfProgressivePause;
  EffectiveToken: IPdfCancellationToken;
begin
  // ... choose EffectiveToken ...

  // Strong ref first, then publish the same object to PDFium via .user.
  Pause.Token := EffectiveToken;
  Pause.Pause.version := 1;
  Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
  Pause.Pause.user := Pointer(EffectiveToken);

การปิดบริบทการเรนเดอร์ไม่ว่าลูปจะจบลงอย่างไร

ทุกครั้งที่เรียก FPDF_RenderPageBitmap_Start จะจัดสรรสถานะแบบก้าวหน้าที่ PDFium เชื่อมโยงกับหน้ากระดาษ และสถานะนั้นจะถูกปล่อยโดย FPDF_RenderPage_Close เท่านั้น มีวิธีออกสามทางจากไดรฟ์ลูป หน้ากระดาษเรนเดอร์เสร็จและสถานะสุดท้ายคือ FPDF_RENDER_DONE โทเค็นสะดุด (trips) และลูปจะออกก่อนกำหนดเพื่อรายงานการยกเลิก บางอย่างล้มเหลวและสถานะคือ FPDF_RENDER_FAILED ทั้งสามกรณีจะต้องเรียก Close และเส้นทางการยกเลิกนั้นง่ายที่สุดที่จะทำผิดพลาด เนื่องจากรูปร่างธรรมชาติของการ "เห็นว่ายกเลิก ให้เบรกออกมา" มีแนวโน้มที่จะข้ามการล้างข้อมูลไปในระหว่างทางออก การปล่อยให้ไปไม่ถึง Close จะทำให้เกิดการรั่วไหล (leaks) ของสถานะต่อหน้า และโปรแกรมดูเอกสารที่ให้ผู้ใช้ยกเลิกการเรนเดอร์แล้วเรนเดอร์เล่า จะสะสมการรั่วไหลนั้นไว้ในทุกหน้าที่ถูกยกเลิก

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

Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
  Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
  while Status = FPDF_RENDER_TOBECONTINUED do
  begin
    if EffectiveToken.IsCancelled then
    begin
      Result := prsCancelled;
      Exit;
    end;
    Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
  end;

  if EffectiveToken.IsCancelled then
    Result := prsCancelled
  else if Status = FPDF_RENDER_DONE then
    Result := prsDone
  else
    Result := prsFailed;
finally
  // Frees the progressive state Start allocated; mandatory on every path.
  FPDF_RenderPage_Close(FPage);
  FPDFBitmap_Destroy(PdfBmp);
end;

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

ผลลัพธ์สามแบบ และสิ่งที่บิตแมปเก็บไว้หลังจากการยกเลิก

จุดเข้าแบบสาธารณะคือ TPdf.RenderPageProgressive และมันจะส่งคืน TPdfProgressiveStatus ซึ่งเป็นหนึ่งใน prsDone, prsCancelled หรือ prsFailed ค่าต่างๆ จะสะท้อนค่าคงที่ (constants) FPDF_RENDER_* ของ PDFium ในสำนวน (idiom) แบบ Pascal แต่จะพับรวมกรณีการยกเลิกเข้าไปเป็นผลลัพธ์ชั้นหนึ่ง (first-class result) แทนที่จะเป็นข้อผิดพลาด

จุดที่ดักจับผู้คนได้ก็คือสิ่งที่บิตแมปปลายทางเก็บไว้หลังจาก prsCancelled มันไม่ว่างเปล่า PDFium จะเรนเดอร์อย่างก้าวหน้าลงในบิตแมปเดียวกันชิ้นแล้วชิ้นเล่า ดังนั้นเมื่อการยกเลิกหยุดลูป บิตแมปจะเก็บรักษาสิ่งที่ถูกวาดลงไปจนถึงช่วงเวลานั้น ซึ่งก็คือรูปภาพบางส่วน (partial image): แถบสี (bands) บางส่วนเสร็จแล้ว ส่วนที่เหลือยังคงแสดงสีที่ใช้เติมอยู่ ผลลัพธ์บางส่วนนั้นจะมีประโยชน์หรือไม่ขึ้นอยู่กับผู้เรียกใช้ โปรแกรมดูเอกสารที่กำลังจะทิ้งบิตแมปนั้นไปเพราะผู้ใช้นำทางไปที่อื่นก็สามารถเพิกเฉยมันไปได้อย่างง่ายดาย โปรแกรมดูเอกสารที่ต้องการแสดงตัวอย่าง (preview) ต้นทุนต่ำก็สามารถเก็บมันไว้ สิ่งที่คุณต้องไม่ทำคือถือเอาเองว่า prsCancelled มีความหมายโดยนัยถึงบิตแมปที่ว่างเปล่าหรือไม่ได้กำหนดค่า; แต่มันคือภาพถ่ายที่เป็นความจริงของการเรนเดอร์ที่ยังไม่เสร็จสมบูรณ์

var
  Bmp: TBitmap;
  Token: IPdfCancellationToken;
  Status: TPdfProgressiveStatus;
begin
  Bmp := TBitmap.Create;
  try
    // Token starts un-cancelled; flip Token.IsCancelled from elsewhere
    // (a UI action, a navigation event) to abort the render in flight.
    Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
    case Status of
      prsDone:      Image1.Picture.Assign(Bmp);  // fully rendered
      prsCancelled: ;                            // partial bitmap, usually discarded
      prsFailed:    ShowMessage('Render failed');
    end;
  finally
    Bmp.Free;
  end;
end;

โทเค็นที่เป็นค่าว่าง (nil token) และเส้นทางโคลแบ็กที่ปราศจากการแยกสาขา

การยกเลิกคือการเลือกเข้าร่วม (opt-in) ผู้เรียกที่แค่ต้องการการเรนเดอร์แบบก้าวหน้าเพื่อประโยชน์ในการสูบข้อความ (message-pumping) โดยไม่มีความตั้งใจที่จะหยุด ควรจะสามารถส่งผ่านค่า nil สำหรับโทเค็นได้ วิธีไร้เดียงสา (naive way) ในการรองรับสิ่งนั้นก็คือการโปรยการตรวจสอบแบบ "หากมีการจัดหาโทเค็นมาให้" เอาไว้ตลอดการโคลแบ็กและในลูป ซึ่งหมายถึงจะมีการแยกสาขาในทุกๆ ชิ้นส่วน (chunk) และโคลแบ็กก็จะต้องรับมือทั้งกรณีที่มีโทเค็นจริงๆ และการไม่มีโทเค็น

การอิมพลีเมนต์หลีกเลี่ยงสิ่งนั้นได้โดยการนำซิงเกิลตัน (singleton) เข้ามาแทนที่เมื่อผู้เรียกไม่ได้ส่งผ่านค่าอะไรมาให้ โทเค็น nil จะถูกสลับเป็น PdfNoCancellationToken ซึ่งเป็นอินเทอร์เฟซที่ IsCancelled เป็นเท็จ (false) เสมอ จากจุดนั้น โคลแบ็กและลูปก็จะมีโทเค็นให้สืบค้นในทุกกรณี ดังนั้นทั้งคู่จึงไม่ต้องมีการตรวจสอบค่า nil และทั้งคู่ก็ไม่จำเป็นต้องมีเส้นทางพิเศษ โทเค็นแบบไม่เคยยกเลิก (never-cancel token) ก็แค่ตอบกลับว่าเท็จเสมอ โคลแบ็กจะส่งคืนศูนย์เสมอ และการเรนเดอร์ก็จะทำงานจนเสร็จสมบูรณ์เหมือนกับการเรนเดอร์ที่ยกเลิกไม่ได้ทุกประการ พฤติกรรมเสริมแบบทางเลือกนี้จะถูกจำลองออกมาในฐานะโทเค็นที่ไม่เคยยิงสัญญาณ แทนที่จะเป็นการไม่มีโทเค็น ซึ่งจะทำให้เส้นทางร้อน (hot path) มีความเป็นระเบียบเรียบร้อย

// nil -> never-cancel singleton, so the callback path is identical
// whether or not the caller opted into cancellation.
if AToken <> nil then
  EffectiveToken := AToken
else
  EffectiveToken := PdfNoCancellationToken;

รูปร่างที่เกิดขึ้นนั้นมีขนาดเล็กและคุ้มค่าที่จะกล่าวซ้ำอีกครั้ง เพราะมันเป็นส่วนที่นำกลับมาใช้ใหม่ได้ ไลบรารี C ที่รองรับโคลแบ็กจะมอบช่องทางให้คุณเพียงช่องทางเดียวในการส่งผ่านสถานะเข้าสู่โคลแบ็กนั้น นั่นคือพอยน์เตอร์ user ที่ทึบแสง ให้นำการอ้างอิงอินเทอร์เฟซ Pascal ที่มีการนับ (counted Pascal interface reference) ไปไว้ด้านหลังพอยน์เตอร์นั้น รักษาการอ้างอิงจริงอันที่สองให้มีชีวิตอยู่ติดกับ struct เพื่อไม่ให้ออบเจ็กต์ถูกเก็บรวบรวมในระหว่างการเรียก และอ่านอินเทอร์เฟซกลับออกมาภายในฟังก์ชัน cdecl แบบคงที่ ห่อหุ้มไดรฟ์ลูปทั้งหมดไว้ใน try และปลดปล่อยเนทีฟคอนเท็กซ์ (native context) ใน finally เทมเพลต (template) เดียวกันนี้จะนำไปใช้กับการดำเนินการ PDFium แบบก้าวหน้าหรือแบบที่ขับเคลื่อนด้วยโคลแบ็กได้ทุกกรณี เมื่อโค้ด Pascal จะต้องเป็นฝ่ายควบคุมอายุการใช้งานในขณะที่ C คอยจับถือพอยน์เตอร์เอาไว้

การยกเลิกเป็นเพียงครึ่งหนึ่งของโปรแกรมดูเอกสารที่ตอบสนองได้ดี อีกครึ่งหนึ่งคือการไม่เรนเดอร์หน้ากระดาษที่คุณเคยวาดไปแล้วซ้ำอีก และการรักษาให้การซูมและการเลื่อนมีความราบรื่นโดยการให้บริการบิตแมปที่แคชไว้ ซึ่งจะครอบคลุมอยู่ใน บทความของเราเกี่ยวกับแคชการเรนเดอร์และประสิทธิภาพของการซูม สำหรับวิธีการที่การเรนเดอร์ที่ยกเลิกได้จะเข้ากันได้กับโปรแกรมดูเอกสารที่สมบูรณ์ไปพร้อมกับการนำทาง (navigation) การเลือก และการค้นหา ให้ดูที่ การสร้างโปรแกรมดู PDF ที่มีคุณลักษณะครบถ้วนด้วย PDFium VCL component การเรนเดอร์แบบก้าวหน้าที่อธิบายไว้ที่นี่มีจัดส่งให้โดยเป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ Lazarus ควบคู่ไปกับ API เพื่อการโหลด การเรนเดอร์ และฟอร์ม ที่ครอบคลุมในส่วนอื่นๆ ของบล็อกนี้