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

เปรียบเทียบเอกสาร PDF แบบคู่ขนานใน Delphi ด้วย PDFium Component

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

เลย์เอาต์ฟอร์ม

VCL ฟอร์มจะรองรับ TScrollBox คอนเทนเนอร์ (containers) สามตัวเคียงข้างกัน โดยแต่ละตัวมี TPdfView อยู่ภายใน และจัดวางแบบ alClient เพื่อให้มันเติมเต็มกล่องพอดี คอมโพเนนต์ TSplitter สองตัววางอยู่ระหว่างกล่องเพื่อให้ผู้ใช้สามารถปรับความกว้างของคอลัมน์ได้ในขณะรันโปรแกรม ทูลบาร์ที่อยู่ด้านบนพาเนลจะมีปุ่มเปิด การควบคุมการซูม และปุ่มสลับการทำงานระหว่างมุมมองแบบสองจอและสามจอ

โหมดสามจอคือเงื่อนไขแบบบูลีนที่ฟอร์มใช้ติดตามอยู่ภายใน เมื่อมีการสลับโหมด คุณจะคำนวณความกว้างใหม่และแสดงหรือซ่อนคอลัมน์ที่สาม แนวทางที่ง่ายที่สุดคือการล้างค่าคุณสมบัติ Align ทั้งหมด ซ่อนตัวแยกหน้าจอ (splitters) จากนั้นกำหนดตำแหน่งสัมบูรณ์:

แผนภาพผังฟอร์มของ viewer เปรียบเทียบ PDF แบบเทียบเคียงของ Delphi ที่สร้างด้วย PDFium Component แสดง toolbar, scroll box สามอันที่มีแผง TPdfView และตัวแบ่งในโหมดสองมุมมองกับสามมุมมอง
พาเนลแต่ละแผงคือ scroll box ที่มี TPdfView อยู่ข้างใน และการสลับระหว่างมุมมองสองมุมกับสามมุมเป็นเพียงชุดการกำหนดความกว้างที่ต่างออกไป
procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // ใช้ค่า (ClientHeight - ความสูงทูลบาร์) เดียวกันกับค่าความสูงทั้งสาม
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

การตั้งค่า Align := alNone ในกล่องทั้งสามใบก่อนการคำนวณตัวเลขทางคณิตศาสตร์จะช่วยหลีกเลี่ยงไม่ให้กลไกข้อจำกัดของ VCL มาต่อต้านการกำหนดค่าของคุณ คืนค่าการมองเห็นของตัวแยกหน้าจอกลับมาหลังจากการจัดวางตำแหน่งเสร็จสิ้นแล้ว หากคุณต้องการลากเพื่อปรับขนาดในโหมดสองจอ

ความสูงของแต่ละกล่องเลื่อน (scroll box) คือพื้นที่ไคลเอนต์ลบความสูงของพาเนลทูลบาร์ เนื่องจากทูลบาร์จะถูกจับคู่เทียบท่า (docked) อยู่ที่ด้านบนด้วย alTop ดังนั้นคำสั่ง ClientHeight - PanelButtons.Height จะให้พื้นที่แนวตั้งที่ใช้งานได้ มอบหมายค่านี้ให้กับกล่องทั้งสามในขณะที่มีการเรียก UpdateLayout เดียวกัน เพื่อที่จะไม่เกิดเฟรมที่กล่องใบใดใบหนึ่งสูงกว่ากล่องใบอื่นจนก่อให้เกิดการกะพริบของเลย์เอาต์

การเปิดเอกสาร

แต่ละคู่พาเนลต้องมีกระบวนการ (procedure) เปิดของตัวเอง รูปแบบนั้นสั้นมาก: ปิดการใช้งานคอมโพเนนต์ กำหนดชื่อไฟล์ เปิดการใช้งานอีกครั้ง จากนั้นตรวจสอบค่า Active; หากยังคงเป็น False ให้เรียกขอรหัสผ่านและลองใหม่อีกครั้ง พึงสังเกตว่า TPdfView.Active เป็นตัวควบคุมการเรนเดอร์ แต่ TPdf.Active คือตัวที่เปิดไฟล์ขึ้นมาจริงๆ; พวกมันต่างทำงานเป็นอิสระต่อกัน การตั้งค่า PdfView.Active := True ในขณะที่ TPdf ที่เชื่อมต่ออยู่ยังไม่ได้ทำงานนั้นไม่มีอันตรายใดๆ แต่มันจะไม่แสดงผลใดๆ เช่นกัน

ผังงานการเปิดเอกสาร PDF ด้วย PDFium Component ใน Delphi แสดงการตรวจ Active แบบเงียบ, การลองรหัสผ่านซ้ำหนึ่งครั้ง และกล่องโต้ตอบ error สำหรับไฟล์ที่เสียหายหรือมีรหัสผ่านป้องกัน
การโหลดที่ล้มเหลวปล่อย Active เป็น False โดยไม่ raise ขั้นตอนจึงตรวจมัน ลองใหม่หนึ่งครั้งด้วยรหัสผ่าน แล้วสุดท้ายรายงานปัญหาแทนที่จะแสดงพาเนลว่างเปล่า
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // ข้อผิดพลาดการโหลดเงียบ: Active ยังคงเป็น False แทนการ raise.
  if not PdfComponent.Active then
  begin
    // น่าจะเป็นไฟล์ที่ป้องกันด้วยรหัสผ่าน; ให้ผู้ใช้ลองอีกหนึ่งครั้ง.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

ตรวจสอบค่า PdfComponent.Active เสมอหลังจากการกำหนดค่า; ไฟล์ที่เสียหายหรือรหัสผ่านที่ไม่ถูกต้องจะทำให้การโหลดล้มเหลวอย่างเงียบๆ โดยไม่แสดงข้อผิดพลาดในเส้นทางเริ่มต้น การตั้งค่า PdfViewComponent.PageNumber := 1 อย่างชัดเจนหลังจากการเปิดที่ประสบความสำเร็จจะช่วยหลีกเลี่ยงหมายเลขหน้าที่ตกค้างมาจากเอกสารก่อนหน้า

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

การติดตามพาเนลที่ทำงาน

เมื่อผู้ใช้คลิกภายในพาเนล พาเนลนั้นจะกลายเป็นพาเนลที่ทำงาน ฟอร์มจะติดตามฟิลด์ FActivePdfView: TPdfView แบบส่วนตัว การตอบรับทางสายตาคือการเปลี่ยนสีขอบของ TScrollBox ที่บรรจุอยู่: ตั้งค่าให้เป็น clHighlight สำหรับพาเนลที่เปิดใช้งานอยู่ และ clWindow สำหรับพาเนลอื่นๆ เชื่อมโยงสิ่งนี้เข้ากับ TPdfView.OnClick แต่ละตัวและกระบวนการเปิดเพื่อให้จุดสนใจติดตามเอกสารที่คุณเพิ่งจะเปิดขึ้นมา

การดำเนินการบางอย่างใช้กับพาเนลที่มองเห็นทั้งหมด แทนที่จะเป็นแค่พาเนลที่ทำงานอยู่เพียงอย่างเดียว ค่าบูลีน FAllViewsMode บนฟอร์มจะทำหน้าที่ขับเคลื่อนเส้นทางนั้น เมื่อเป็นความจริง (true) การเปลี่ยนระดับการซูมและการเลื่อนหน้าจะกระจายออกไปยังทุกพาเนลที่มีเอกสารเปิดใช้งานอยู่:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

การเลื่อนหน้าแบบซิงโครไนซ์ (Synchronized Page Navigation)

การเลื่อนหน้าแบบซิงโครไนซ์เป็นเพียงทางเลือกหนึ่งแต่ก็มีประโยชน์สำหรับเวิร์กโฟลว์การแก้ไขเอกสารที่ทั้งสองไฟล์มีช่วงของหน้ากระดาษที่เท่ากัน ตรรกะนี้อยู่ในตัวจัดการเหตุการณ์ (event handler) ที่จะทำงานหลังจากผู้ใช้ทำการเลื่อนหน้าจอมุมมองหนึ่ง เมื่อมุมมองต้นทางเปลี่ยนค่า PageNumber ตัวจัดการจะเผยแพร่หมายเลขนั้นไปยังมุมมองอื่นๆ ภายใต้ข้อจำกัดหนึ่งคือ: มุมมองเป้าหมายต้องมีจำนวนหน้าเท่ากับหรือมากกว่าค่านั้นเป็นอย่างน้อย มิฉะนั้นก็จะข้ามไป

PageNumber ใน TPdfView และ TPdf นั้นทำงานเป็นอิสระต่อกัน TPdf.PageNumber ติดตามว่าหน้าใดที่คอมโพเนนต์เอกสารถือว่าเป็นหน้าปัจจุบัน; TPdfView.PageNumber ติดตามสิ่งที่กำลังแสดงผลอยู่บนหน้าจอ เพื่อจุดประสงค์ด้านการเลื่อนหน้าจอ คุณจะต้องใช้คุณสมบัติของมุมมอง (view property) ไม่ใช่คุณสมบัติเอกสาร (document property)

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

สิ่งหนึ่งที่ต้องระวัง: การตั้งค่า PdfView.PageNumber ผ่านทางโปรแกรมในตัวจัดการซิงค์ก็จะก่อให้เกิดเหตุการณ์การเปลี่ยนแปลงในมุมมองนั้นเช่นกัน ป้องกันการเรียกตัวเองซ้ำอย่างไม่สิ้นสุด (infinite recursion) ด้วยแฟล็กแบบบูลีนที่คุณตั้งค่าไว้ก่อนการมอบหมาย (assignment) แล้วรีบล้างออกทันทีหลังจากนั้น แฟล็กดังกล่าวใช้กับเฉพาะฟอร์ม (per-form) ไม่ใช่กับแต่ละมุมมอง (per-view) เนื่องจากมุมมองทั้งสามจะใช้ตัวจัดการแบบเดียวกัน

แผนภาพการนำทางหน้าแบบซิงค์กันใน viewer เปรียบเทียบ PDF ของ Delphi ด้วย PDFium Component พร้อม checkbox sync, guard จำนวนหน้าต่อมุมมองเป้าหมาย และ flag กัน recursion
หมายเลขหน้าเดินทางจากมุมมองต้นทางไปยังมุมมองอื่นทุกมุมมองเฉพาะเมื่อเปิด sync และมุมมองเป้าหมายแต่ละมุมมองบรรจุหน้านั้นจริง ๆ

การซูมแบบต่อพาเนล (Zoom Per Panel)

TPdfView แต่ละตัวมีคุณสมบัติ Zoom ของมันเอง ซึ่งเป็นค่าประเภท Double ในรูปแบบเปอร์เซ็นต์ โดยที่ Zoom := 100 หมายถึงขนาดที่แท้จริง (100%) การตั้งค่าในส่วนนี้จะไปเขียนทับข้อมูลส่วน FitMode ใดๆ ที่กำลังทำงานอยู่ สำหรับปุ่มปรับความกว้างให้พอดีหน้าจอบนพาเนลที่ทำงานอยู่ ให้อ่านการซูมแบบพอดี (fit zoom) จากคำสั่ง PdfView.PageWidthZoom[PdfView.PageNumber] แล้วกำหนดค่าลงไป สำหรับปุ่มปรับให้พอดีหน้า ให้ใช้คำสั่ง PageZoom[PageNumber] ทั้งสองตัวเป็นคุณสมบัติในรูปแบบอาร์เรย์ที่ใช้หมายเลขหน้าแบบ 1-based ในการทำดัชนี ดังนั้นคุณจึงควรป้องกันไม่ให้หมายเลขหน้าเป็นศูนย์ก่อนที่จะทำการเข้าถึง

เมื่อคุณส่งออกหน้าที่แสดงผลปัจจุบันไปยังรูปแบบรูปภาพ คุณจะอ่านค่าการหมุน (rotation) จากมุมมอง แต่เรียก RenderPage ไปที่คอมโพเนนต์ TPdf ไม่ใช่มุมมอง รูปแบบบิตแมปของ TPdf.RenderPage จะรับขนาดพิกเซลที่ชัดเจนบวกกับค่า TRotation และการตั้งค่า TRenderOptions ค่าแปรผันของฟังก์ชัน (function variant) จะส่งกลับคืน TBitmap ที่มีผู้เรียกเป็นเจ้าของซึ่งคุณจะปลดปล่อยมันเองหลังจากทำการบันทึก:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

ตัวคูณ 2x บนความกว้างและความสูงจะให้ผลลัพธ์ที่คมชัดยิ่งขึ้นสำหรับเอกสารที่มีข้อความตัวเล็กๆ try/finally ในช่วงของบิตแมปที่ว่างอยู่นั้นไม่ใช่สิ่งที่เป็นทางเลือก; การกดยกเลิกใน TSaveDialog ก็ยังคงทำงานในบล็อก finally และคุณต้องการให้ปล่อยบิตแมปนั้นออกมาไม่ว่าผู้ใช้จะทำสิ่งใดก็ตาม

ข้อกำหนดของ DLL

PDFium Component ห่อหุ้มไลบรารี pdfium ดั้งเดิมเอาไว้ โปรเซสโฮสต์ 32-บิต ต้องการ pdfium32.dll; โฮสต์ 64-บิต ต้องการ pdfium64.dll รุ่นที่มีเอนจิน V8 JavaScript จะมีการเติมคำต่อท้าย v8 และมีน้ำหนักประมาณ 23-27 MB เมื่อเทียบกับรุ่นมาตรฐานที่มีขนาด 5-6 MB สำหรับโปรแกรมเปิดดูแบบเปรียบเทียบข้อมูลที่ปิดการทำงานของการกรอกแบบฟอร์ม (Pdf.FormFill := False) การใช้รุ่นมาตรฐานที่ไม่มี V8 ก็เพียงพอแล้ว และยังช่วยให้ขนาดไฟล์แจกจ่ายมีขนาดเล็กลงอีกด้วย

วางไฟล์ DLL ไว้ในไดเร็กทอรีเดียวกับไฟล์ปฏิบัติการ (executable) หรือในไดเร็กทอรีใดๆ บนระบบ PATH คอมโพเนนต์จะโหลดไฟล์นี้ตามความต้องการเมื่อมีการใช้งาน TPdf ตัวแรก ดังนั้นการที่ไฟล์ DLL ขาดหายไปจึงมักจะปรากฏให้เห็นในจุดนั้น แทนที่จะเป็นตอนที่แอปพลิเคชันเริ่มต้นการทำงาน หากคุณจัดส่งตัวติดตั้ง (installer) แนวทางที่น่าเชื่อถือที่สุดคือการคัดลอกไฟล์ DLL ลงในโฟลเดอร์แอปพลิเคชันระหว่างขั้นตอนการติดตั้ง แทนที่จะพึ่งพาไดเร็กทอรีของระบบที่ผู้ดูแลระบบอาจเข้ามาทำความสะอาดทิ้งในภายหลัง

รุ่น V8 จะมีประโยชน์เป็นหลักเมื่อคุณต้องการโต้ตอบกับการกระทำของ PDF JavaScript เช่น เพื่อเปิดการทำงานฟิลด์การคำนวณ หรือส่งข้อมูลผ่านแฮนเดิล โปรแกรมดูแบบเปรียบเทียบที่เป็นแบบพาสซีฟไม่มีเหตุผลให้ต้องรัน JavaScript; การตั้งค่า Pdf.FormFill := False ก่อนที่จะตั้งค่า Active := True จะเป็นการข้ามสภาพแวดล้อมการกรอกแบบฟอร์มทั้งหมด ซึ่งนั่นก็หมายความว่าจะไม่มีเอนจิน JS ถูกเปิดขึ้นมาทำงานเลยแม้ว่าจะใช้รุ่นมาตรฐานก็ตาม นั่นคือค่าเริ่มต้นที่ถูกต้องสำหรับโปรแกรมดูแบบอ่านได้อย่างเดียว ไม่ว่าคุณจะส่ง DLL รุ่นใดมาให้ก็ตาม

สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับ PDFium Component และ API ตัวเต็ม คุณสามารถเข้าดูได้ที่หน้าเพจผลิตภัณฑ์ของ Delphi PDFium Component