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

สร้างโปรแกรมอ่าน PDF ใน Delphi ด้วย PDFium Component

โปรแกรมอ่าน PDF ใน Delphi นั้นโดยพื้นฐานประกอบด้วยสองคอมโพเนนต์และการเชื่อมต่อระหว่างกัน TPdf เป็นเจ้าของเอกสาร: มันทำหน้าที่เปิดไฟล์, ถอดรหัสลับ, และตอบคำถามเกี่ยวกับจำนวนหน้าและข้อมูลเมตา (metadata) ส่วน TPdfView เป็นคอนโทรลสำหรับแสดงผลที่ทำหน้าที่วาดหน้าต่างๆ ลงบนหน้าจอและจัดการการเลื่อนดู, การซูม, และหน้าที่ผู้ใช้กำลังดูอยู่ PDFium Component ครอบเอนจินการเรนเดอร์ตัวเดียวกับที่อยู่ใน Chrome ดังนั้น glyphs, anti-aliasing และสีที่คุณได้บนแคนวาสจะตรงกับสิ่งที่ผู้ใช้ของคุณเห็นในเบราว์เซอร์ของพวกเขา งานไม่ได้อยู่ที่การเรนเดอร์ แต่อยู่ที่การเชื่อมต่ออ็อบเจกต์เอกสารไปยังส่วนแสดงผล, การโหลดโดยไม่เกิดข้อผิดพลาดกับไฟล์ที่เสียหายหรือมีการป้องกันด้วยรหัสผ่าน, และการให้ผู้ใช้มีคอนโทรลเพียงเล็กน้อยที่ทำให้โปรแกรมอ่านรู้สึกสมบูรณ์: การเปลี่ยนหน้า, การเปลี่ยนการซูม, การปรับขนาดหน้าให้พอดีกับหน้าต่าง

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

การเชื่อมต่อ TPdf ไปยัง TPdfView

วาง TPdf และ TPdfView ลงบนฟอร์ม จากนั้นบอกส่วนแสดงผลว่าจะให้แสดงเอกสารใด การกำหนดค่าเพียงครั้งเดียวนั้นคือลิงก์ทั้งหมดระหว่างเอกสารที่ไม่ใช่การแสดงผลและคอนโทรลที่วาดมัน

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

ก่อนที่โค้ดใดๆ เหล่านี้จะทำงาน ไลบรารีเนทีฟ (native library) ของ PDFium จะต้องอยู่บนเครื่องก่อน PDFium Component จะเรียกใช้ pdfium32.dll หรือ pdfium64.dll ขึ้นอยู่กับแพลตฟอร์มเป้าหมายของคุณ และเอกสารจะปฏิเสธการเปิดหากไม่พบ DLL ดังนั้นคุณควรจัดส่ง DLL ที่ตรงกันไปพร้อมกับไฟล์เรียกทำงานของคุณ หรือวางไว้ในที่ที่ระบบสามารถโหลดมันพบ รุ่นที่มีการเปิดใช้งาน V8 (V8-enabled builds) มีไว้สำหรับ PDF ที่มี JavaScript ซึ่งคุณต้องการรันเท่านั้น ซึ่งโปรแกรมอ่านแบบธรรมดาไม่มีความจำเป็น ดังนั้นให้เลือกใช้ DLL มาตรฐานเว้นแต่คุณจะมีเหตุผลที่ชัดเจนในการไม่ใช้งาน

การโหลดเอกสารโดยไม่เชื่อถือข้อมูลที่ป้อนเข้ามา

สัญชาตญาณมักจะเป็นการห่อหุ้มการโหลดไว้ใน try/except และถือว่าข้อยกเว้นที่ถูกส่งออกมาเป็นความล้มเหลว สัญชาตญาณนั้นผิดที่นี่ และการทำผิดพลาดนี้จะสร้างโปรแกรมอ่านที่ดูปกติดีจนกระทั่งมีคนป้อนไฟล์ที่เสียเข้าไป การตั้งค่า Active := True จะไม่ทำให้เกิดข้อยกเว้นเมื่อโหลดล้มเหลว PDFium Component จะดักจับข้อผิดพลาดภายในและปล่อยให้ Active อยู่ที่สถานะ False ดังนั้นวิธีเดียวที่ซื่อตรงในการรู้ว่าเอกสารถูกเปิดขึ้นมาหรือไม่คือการอ่านพร็อพเพอร์ตี้กลับมาหลังจากที่คุณตั้งค่ามัน

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

มีสองสิ่งที่ควรให้ความสนใจ สิ่งแรกคือ PageNumber มีอยู่บนทั้งสองอ็อบเจกต์และทั้งสองแยกจากกัน Pdf.PageNumber คือแนวคิดของเอกสารเกี่ยวกับหน้าปัจจุบัน; ส่วน PdfView.PageNumber คือหน้าที่คอนโทรลแสดงผลจริงๆ และมันคือสิ่งที่คุณต้องตั้งค่าเพื่อเลื่อนผู้ใช้ไปตามไฟล์ การตั้งค่าอันหนึ่งจะไม่เปลี่ยนอันอื่น ดังนั้นโปรแกรมอ่านจะขับเคลื่อนพร็อพเพอร์ตี้ของส่วนแสดงผลเสมอ สิ่งที่สองคือการจัดทำดัชนีเริ่มต้นที่ 1: หน้าจะรันตั้งแต่ 1 ถึง Pdf.PageCount ไม่ใช่จาก 0 ซึ่งอาจทำให้คนที่คุ้นเคยกับอาร์เรย์ที่เริ่มต้นที่ศูนย์สับสนได้

การจัดการไฟล์ที่เข้ารหัสลับ

เอกสารที่เข้ารหัสลับจะรวมอยู่ในเส้นทางการโหลดเดียวกัน หากมีการตั้งรหัสผ่านสำหรับเปิดก่อนการเปิดใช้งาน (activation) เอกสารจะถอดรหัสลับขณะที่มันเปิด หากรหัสผ่านผิดหรือไม่มี Active จะยังคงเป็น False เหมือนกับไฟล์ที่เสีย ดังนั้นวิธีการกู้คืนคือการขอให้ใส่รหัสผ่านและลองเปิดใช้งานอีกครั้ง

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // must be set before Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

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

การเปลี่ยนหน้าผ่านเอกสาร

เมื่อเอกสารเปิดขึ้นมา การนำทางคือการคำนวณทางคณิตศาสตร์บน PdfView.PageNumber ที่ถูกจำกัดขอบเขตด้วย Pdf.PageCount งานจริงเพียงอย่างเดียวคือการจำกัดขอบเขต (clamping) เพื่อให้ปุ่มไม่ดันหน้าออกนอกช่วง และปุ่มไปหน้าแรกและหน้าสุดท้ายยังคงถูกปิดการใช้งานเมื่ออยู่ที่ส่วนท้ายของไฟล์

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// the four navigation buttons reduce to one call each
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

กล่องข้อความ "ไปที่หน้า N" ก็เป็นการเรียกใช้ GoToPage แบบเดียวกันที่ป้อนด้วยจำนวนเต็มที่แยกวิเคราะห์มา และการจำกัดขอบเขตจะครอบคลุมกรณีที่ผู้ใช้พิมพ์ 9999 เข้าไปในไฟล์ที่มีสิบหน้า ควรให้ UpdatePageLabel เป็นที่เดียวที่เขียน "หน้า 3 จาก 12" ดังนั้นการอ่านตัวเลขจะไม่มีทางไม่สอดคล้องกับสิ่งที่แสดงผลในหน้าจอ

การซูม: เปอร์เซ็นต์ที่ชัดเจนและโหมดพอดี

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

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

นี่คือส่วนที่ทำให้หลายคนสะดุด การกำหนด Zoom โดยตรงจะรีเซ็ต FitMode เป็น pfmNone นั่นคือการทำงานที่ถูกต้อง ไม่ใช่ข้อผิดพลาด (bug): ทันทีที่ผู้ใช้เลือกกำหนดให้เป็น 150% อย่างแม่นยำ ส่วนแสดงผลก็จะไม่สามารถรักษา "ปรับให้พอดีความกว้าง" ได้อีกต่อไป เนื่องจากคำขอทั้งสองขัดแย้งกัน ผลที่ตามมาสำหรับ UI ของคุณคือปุ่มซูมเข้าและปุ่มปรับให้พอดีหน้าต่างมีสถานะที่ใช้งานร่วมกันไม่ได้ และแถบเครื่องมือควรแสดงให้เห็นโหมดที่กำลังใช้งานอยู่ เมื่อผู้ใช้คลิกปรับให้พอดีหน้าต่าง ให้ตั้งค่า FitMode; เมื่อพวกเขาคลิกการซูมแบบตัวเลข ให้ตั้งค่า Zoom และปล่อยให้มันล้างโหมดความพอดีด้วยตัวมันเอง

หากคุณต้องการคำนวณค่าพอดีด้วยตัวเอง อาจจะเพื่อใส่เปอร์เซ็นต์พอดีปัจจุบันลงในสไลเดอร์ซูม ตัวช่วยสำหรับแต่ละหน้าจะให้ตัวเลขเหล่านั้นโดยไม่เปลี่ยนโหมด PageWidthZoom[N], PageZoom[N], และ ActualSizeZoom[N] จะคืนค่าเปอร์เซ็นต์ที่จะปรับหน้า N ให้พอดีกับความกว้าง, ปรับให้พอดีทั้งหน้า, หรือเรนเดอร์ตามขนาดจริง

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

สิ่งที่โปรแกรมอ่านที่สมบูรณ์ต้องการจริงๆ

โปรแกรมอ่านด้านบนมีเพียงไม่กี่สิบบรรทัด และมันก็ทำหน้าที่ตามที่เวิร์กโฟลว์เอกสารต้องการ: เปิดไฟล์, อยู่รอดจากไฟล์ที่เสียหาย, แสดงหน้าเอกสาร, เลื่อนระหว่างหน้า, และเปลี่ยนการขยายด้วยมือหรือตามความพอดี PDFium จัดการกับส่วนที่ยากๆ อย่างเงียบๆ ฟอนต์ที่ฝังมาได้รับการประมวลผล คำอธิบายประกอบ (annotations) และช่องฟอร์มวาดลงตรงจุดที่เอกสารวางตำแหน่งไว้ และหน้าที่คุณเห็นจะตรงกับหน้าตาที่ผู้ใช้ Chrome จะได้เห็น เพราะนี่คือเอนจินเดียวกันในการวาดภาพทั้งคู่

จากพื้นฐานนี้ การเพิ่มเติมเป็นสิ่งที่เพิ่มเข้ามาเป็นส่วนๆ แทนที่จะเป็นเชิงโครงสร้าง การเลือกและค้นหาข้อความอ่านจากเลเยอร์ข้อความ (text layer) เดียวกันกับที่ PDFium สร้างขึ้นมาอยู่แล้ว ข้อมูลเมตา (metadata) เช่น Pdf.Title และ Pdf.Author เป็นแค่พร็อพเพอร์ตี้ที่อ่านได้ง่าย การหมุนและการแปลงเป็นสีเทาเป็นตัวเลือกการเรนเดอร์ที่คุณส่งผ่านเมื่อวาดหน้าเอกสารลงบิตแมป สิ่งเหล่านี้ทั้งหมดไม่ได้เปลี่ยนแกนหลักที่คุณมีอยู่ ซึ่งประกอบด้วยอ็อบเจกต์เอกสาร, ส่วนแสดงผล, และกระบวนการทำงานที่เริ่มต้นจากการโหลดและตามด้วยการนำทาง ที่เชื่อมต่อพวกมันเข้าด้วยกัน จัดทำแกนหลักนี้ให้ถูกต้องแล้วส่วนที่เหลือก็เป็นแค่ส่วนตกแต่ง

คอมโพเนนต์ TPdf และ TPdfView ที่ถูกใช้ตลอดบทความนี้เป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ C++Builder ซึ่งมีการอ้างอิงโปรแกรมอ่านเต็มรูปแบบอยู่บนหน้าผลิตภัณฑ์