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

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

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

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

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

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

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;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  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 ที่เชื่อมต่ออยู่ยังไม่ได้ทำงานนั้นไม่มีอันตรายใดๆ แต่มันจะไม่แสดงผลใดๆ เช่นกัน

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;

  // Load failures are silent: Active stays False instead of raising.
  if not PdfComponent.Active then
  begin
    // Most likely a password-protected file; give the user one retry.
    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) เนื่องจากมุมมองทั้งสามจะใช้ตัวจัดการแบบเดียวกัน

การซูมแบบต่อพาเนล (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