Artikel Teknis

Memecah Dokumen PDF dengan PDFium Component di Delphi

PDFium Component memberi Anda satu metode untuk pemecahan PDF: ImportPages. Selebihnya, entah Anda sedang mengisolasi satu halaman, memotong pada batas sembarang, atau mengikuti struktur bookmark milik dokumen itu sendiri, hanyalah cara berbeda untuk memutuskan nomor halaman mana yang masuk ke setiap file keluaran. Mekanismenya tetap sama. Memahami itu sejak awal menghemat banyak belokan yang keliru

Cara kerja loop pemecahannya

Polanya sama tanpa peduli bagaimana Anda membagi dokumen sumbernya. Buat sebuah instance TPdf yang baru, panggil CreateDocument padanya untuk menginisialisasi PDF kosong di memori, impor halaman yang Anda inginkan dengan ImportPages, simpan hasilnya, lalu setel ulang Active menjadi False sebelum iterasi berikutnya. Langkah terakhir itulah yang sering terlewat: CreateDocument tidak menutup secara tersirat dokumen yang masih ada di memori, jadi Anda harus menyimpan keluaran Anda lalu menyetel ulang Active := False secara eksplisit sebelum memanggilnya lagi; menyetel ulang lebih dulu menjaga keadaannya tetap bersih dan terdefinisi jelas. Instance TPdf di lapisan luar dipakai ulang di seluruh iterasi, dan itu menjaga tekanan alokasi tetap rendah pada pekerjaan besar

Diagram loop pemecahan PDFium Component di Delphi: CreateDocument, ImportPages dari sumber yang hanya-baca, sebuah SaveAs yang diperiksa, dan reset Active sebelum tiap iterasi baru
Apa pun yang menentukan kelompoknya, loop-nya tetap identik: impor halamannya, simpan dengan hasilnya diperiksa, lalu setel ulang Active supaya CreateDocument berikutnya bermula dari keadaan yang bersih

Beginilah rupa pemecahan halaman demi halaman ketika dikupas sampai intinya:

procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 1 to Source.PageCount do
    begin
      PdfOut.CreateDocument;

      // Range adalah string nomor halaman berbasis 1; titik sisip 1 = posisi pertama
      if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
        raise Exception.CreateFmt('Failed to import page %d', [I]);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;   // setel ulang sebelum CreateDocument berikutnya
    end;
  finally
    PdfOut.Free;
  end;
end;

Parameter Range pada ImportPages memakai format string yang sama seperti yang dipakai PDFium secara internal: daftar nomor halaman yang dipisah koma atau rentang yang dibatasi tanda hubung, semuanya berbasis 1. '3' mengimpor halaman 3. '1-5' mengimpor halaman 1 sampai 5 secara berurutan. '2,5,8' mengimpor ketiga halaman itu. Parameter ketiganya adalah posisi penyisipan berbasis 1 di dalam dokumen tujuan; memberikan 1 selalu menempatkan halaman yang diimpor di awal sebuah file yang selain itu kosong, dan itulah yang Anda inginkan di sini

Memecah berdasarkan rentang halaman

Ketika pemanggilnya menyodorkan daftar seperti 1-12,13-24,25-36, Anda mem-parsingnya menjadi pasangan awal/akhir lalu menjalankan loop yang sama, dengan merangkai string rentangnya dari setiap pasangan:

procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
  const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(RangeList) do
    begin
      PdfOut.CreateDocument;
      if not PdfOut.ImportPages(Source, RangeList[I], 1) then
        raise Exception.Create('Invalid page range: ' + RangeList[I]);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Validasi sebelum Anda sampai ke ImportPages itu penting di sini. ImportPages mengembalikan False ketika sebuah nomor halaman di dalam string rentangnya melebihi Source.PageCount, namun ia tidak melempar exception dan tidak menghasilkan file keluaran separuh jadi yang bisa Anda kenali hanya dari namanya. Periksa nilai kembalian SaveAs dan catat kegagalannya secara terpisah; sebuah rentang yang menghasilkan file keluaran kosong tidak tampak salah sampai ada yang membukanya

Memecah pada batas bookmark

Pendekatan ketiga memakai struktur milik dokumen itu sendiri alih-alih daftar yang disodorkan dari luar. Setiap bookmark tingkat teratas membawa nomor halaman sasarannya; bagian yang didefinisikannya berjalan dari halaman itu sampai satu halaman sebelum halaman bookmark berikutnya, atau sampai akhir dokumen untuk entri terakhirnya

Diagram yang memetakan bookmark PDF tingkat teratas ke rentang halaman terhitung dan file keluaran saat memecah dengan PDFium Component di Delphi, termasuk sebuah bookmark di luar rentang yang dilewati
Sebuah bagian berjalan dari halaman setiap bookmark tingkat teratas sampai satu halaman sebelum bookmark berikutnya, dan entri yang menunjuk melewati ujungnya dilewati alih-alih menghasilkan file kosong
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
  Bm: TBookmarks;
  I, StartPage, EndPage: Integer;
  PdfOut: TPdf;
  RangeStr, OutFile, SafeTitle: string;
begin
  Bm := Source.Bookmarks;
  if Length(Bm) = 0 then
    Exit;

  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(Bm) do
    begin
      StartPage := Bm[I].PageNumber;
      if I < High(Bm) then
        EndPage := Bm[I + 1].PageNumber - 1
      else
        EndPage := Source.PageCount;

      if (StartPage < 1) or (EndPage < StartPage) then
        Continue;

      RangeStr := Format('%d-%d', [StartPage, EndPage]);

      PdfOut.CreateDocument;
      if not PdfOut.ImportPages(Source, RangeStr, 1) then
      begin
        PdfOut.Active := False;
        Continue;   // lewati bagian cacat alih-alih menulis file kosong
      end;

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Dokumen yang tidak punya bookmark bukanlah kondisi error yang layak disodorkan kepada pengguna sebagai error; itu sekadar berarti mode pemecahan ini tidak punya bahan untuk dikerjakan. Penjaga Length(Bm) = 0 menanganinya tanpa ribut. Yang layak disodorkan adalah ketika nomor halaman sebuah bookmark berada di luar rentang dokumennya, yang terjadi pada file cacat tempat outline-nya tidak pernah diperbarui setelah halaman dihapus. Pemeriksaan batas pada StartPage dan EndPage melewati entri semacam itu alih-alih meneruskan rentang sampah ke ImportPages

Penamaan file keluaran dan reset Active

Keamanan nama file untuk nama yang diturunkan dari bookmark butuh perhatian tersendiri. Judul bookmark bisa memuat karakter yang sah di dalam sebuah string PDF namun tidak sah di dalam path filesystem. Setidaknya, gantilah garis miring, garis miring terbalik, dan titik dua sebelum merangkai path keluarannya. Di Windows, *, ?, ", <, >, dan | juga terlarang; sebuah loop sederhana atas himpunan tetap sudah mencakup semuanya tanpa perlu menarik regex

Baris Active := False di akhir setiap iterasi pantas ditekankan karena itulah satu-satunya syarat yang tidak kentara dalam pola ini. CreateDocument tidak menutup secara tersirat apa pun yang sedang terbuka. Jika Active masih bernilai True ketika CreateDocument berjalan lagi, dokumen yang masih ada di memori tidak pernah ditutup atau disimpan dengan benar, dan Anda tidak bisa mengandalkan perilaku yang terdefinisi jelas dalam keadaan itu, jadi simpan dan setel ulang secara eksplisit sebelum memulai dokumen berikutnya. Anggaplah ia pasangan bagi try/finally: blok finally membebaskan objek luarnya; Active := False menyetel ulang keadaan dokumen di dalamnya di antara iterasi loop

Pemakaian memori di sepanjang pekerjaan pemecahan yang besar tetap datar dengan pendekatan ini karena Anda tidak pernah menahan lebih dari satu dokumen keluaran di memori sekaligus. Dokumen sumbernya tetap terbuka dan hanya-baca sepanjang prosesnya; ImportPages menyalin data halamannya ke dalam dokumen baru tanpa mengubah sumbernya. Jika sumbernya terenkripsi, bukalah dengan password-nya sebelum loop-nya, dan halaman yang disalin ke setiap file keluaran akan tidak terenkripsi, yang biasanya merupakan perilaku yang tepat untuk keluaran pecahan yang dibagikan ke penerima yang berbeda-beda

Satu hal lagi tentang SaveAs: ia mengembalikan sebuah Boolean. Direktori keluaran yang tidak ada, sebuah path dengan karakter yang ditolak OS, atau kondisi disk penuh semuanya akan membuat SaveAs mengembalikan False tanpa melempar exception. Dalam pekerjaan batch yang memecah dokumen 200 halaman menjadi 200 file satu halaman, kegagalan senyap pada halaman 147 mudah terlewat. Periksa nilai kembaliannya pada setiap panggilan lalu hitung keberhasilannya terhadap total yang diharapkan ketika loop-nya selesai

Metode ImportPages dan CreateDocument yang ditampilkan di sini adalah bagian dari PDFium Component untuk Delphi dan C++Builder