Artikel Teknis

Flatten Rotasi Halaman PDF tanpa Merusak Box di Delphi

HotPDF meratakan rotasi halaman PDF dengan THotPDF.FlattenLoadedPageRotation: method ini membungkus konten setiap halaman terotasi dalam transform cm searah jarum jam, menulis ulang setiap page box yang benar-benar dimiliki halaman, memutar geometri anotasi, matrix appearance, destination eksplisit, dan geometri struktur bertag sebesar sudut yang sama, lalu menyetel /Rotate ke 0. Halaman terlihat identik di viewer, tapi sistem koordinatnya kini tegak. Itu penting begitu sebuah tool di hilir, print RIP, atau kode stamping Anda mengabaikan /Rotate dan menaruh sesuatu di user space mentah

Pemicu yang khas adalah scanner atau aplikasi capture mobile yang menulis halaman landscape sebagai media portrait dengan /Rotate 90. Semua viewer menampilkannya dengan benar, jadi tak ada yang sadar sampai seseorang mencap nomor halaman di "kanan bawah" dan hasilnya miring di sepanjang tepi kiri, atau langkah imposition yang hanya membaca /MediaBox menyusun slot portrait untuk halaman landscape. Flatten terdengar seperti pekerjaan matrix satu baris. Dalam praktik ia menyentuh lima page box, tiga jenis geometri anotasi, target link dokumen, dan structure tree, dan masing-masing punya aturannya sendiri di ISO 32000-1

Arah mana yang diputar /Rotate pada halaman PDF?

/Rotate memutar halaman searah jarum jam untuk tampilan dan pencetakan, dalam kelipatan 90 derajat (ISO 32000-1 §7.7.3.3, Tabel 30). Pada 90 derajat tepi kiri media menjadi bagian atas dan tepi atas menjadi sisi kanan, sehingga dalam device space y-ke-bawah pemetaannya adalah X = (y - Bottom) * Scale dan Y = (x - Left) * Scale. Pada 270 derajat tepi kanan menjadi bagian atas. /Rotate juga salah satu dari hanya empat atribut halaman yang bisa diwarisi, bersama /Resources, /MediaBox, dan /CropBox (§7.7.3.4), jadi dictionary halaman tanpa /Rotate sendiri tetap bisa diputar oleh leluhur /Pages. THotPDF.GetLoadedPageRotation menelusuri rantai /Parent dan menormalkan hasilnya ke 0–359, itulah nilai yang Anda mau, bukan key mentah di halaman

Arahnya gampang salah dengan cara yang lolos dari pengujian, dan build HotPDF sebelumnya persis mengalaminya. Matrix halaman-ke-perangkat lama menukar komponen y untuk 90 dan 270, yang menghasilkan refleksi melintasi diagonal alih-alih rotasi: orientasi matrix-nya terbalik relatif terhadap kasus tanpa rotasi. Kedua sudut tetap "terlihat terotasi", bitmap punya lebar dan tinggi yang tertukar, dan round trip dari halaman ke tampilan lalu kembali mengembalikan titik awal, jadi pengecekan dimensi dan test round-trip semuanya lolos. Satu-satunya pemeriksaan yang bisa diandalkan adalah ke mana penanda sudut mendarat, dibandingkan piksel demi piksel terhadap renderer referensi. Karena model viewer, backend render SIMD, dan mapping highlight saling menyalin matrix yang sama, semuanya diperbaiki bersamaan, dan kode flattening kini memakai konvensi searah jarum jam yang sama dengan renderer

Cara HotPDF meratakan rotasi halaman di Delphi: halaman portrait yang tersimpan dengan /Rotate 90 tampil searah jarum jam sebagai tampilan landscape 792 kali 612, pemetaan perangkat X = (y - Bottom) * Scale, Y = (x - Left) * Scale memindahkan setiap sudut, dan penukaran komponen y matrix menghasilkan refleksi yang hanya tertangkap oleh perbandingan penanda sudut
Viewer memutar halaman searah jarum jam untuk tampilan sementara byte-nya tetap portrait — GetLoadedPageRotation menelusuri rantai /Parent lebih dulu, karena /Rotate salah satu dari empat atribut halaman yang bisa diwarisi

Cara FlattenLoadedPageRotation menulis ulang sebuah halaman

FlattenLoadedPageRotation(PageRange, Info) memproses setiap halaman di PageRange yang rotasi efektifnya 90, 180, atau 270, dan mengembalikan jumlah halaman yang diratakannya. PageRange kosong berarti semua halaman; kalau tidak, string memakai sintaks berbasis satu yang biasa, '1-3,7', dan nomor halaman di luar jangkauan melempar exception alih-alih dilewati. Content stream aslinya tak pernah dienkode ulang. Method ini menambahkan di depan sebuah stream baru berisi q 0 -1 1 0 -Bottom Width+Left cm (untuk 90 derajat) ke /Contents halaman, menambahkan stream berisi Q di belakang, dan akhirnya menulis /Rotate 0 eksplisit ke dictionary halaman supaya nilai warisan di node /Pages tak bisa memutar halaman untuk kedua kalinya

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = semua halaman; halaman 0 derajat ikut discan tapi dibiarkan
      Flattened := Pdf.FlattenLoadedPageRotation('', Info);
      Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
      Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
        [Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
         Info.TransformedStructureGeometryCount]));
      if Flattened > 0 then
        Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Record THPDFRotationFlattenInfo layak dicatat log alih-alih dibuang. ScannedPageCount adalah ukuran rentangnya, FlattenedPageCount sama dengan return value, dan tiga counter Transformed... memberi tahu apakah dokumen punya link, bookmark, atau geometri bertag yang menunjuk halaman-halaman yang diputar. Batch yang setiap filenya melaporkan nol destination tak masalah; file PDF/UA bertag yang melaporkan nol geometri struktur padahal Anda mengharapkan bounding box figure adalah sinyal untuk menginspeksinya secara manual

Page box mana yang ditulis ulang flattening, dan dalam urutan apa?

Flattening hanya menulis ulang box yang sudah dimiliki halaman, dan ia membaca setiap box sebelum menulis salah satunya. Urutan itu penting karena rantai default: GetLoadedPageBox(PageIndex, pbCropBox, ...) mengembalikan /MediaBox ketika halaman tak punya /CropBox, dan /BleedBox, /TrimBox, serta /ArtBox default ke CropBox (§14.11.2). Versi sebelumnya memang membaca, mentransform, dan menulis satu box dalam satu waktu. Ia menulis ulang MediaBox lebih dulu, lalu membaca "CropBox", mendapatkan MediaBox yang sudah diputar, memutarnya untuk kedua kalinya, dan menulis CropBox yang tak pernah dimiliki halaman, yang memotong halaman landscape menjadi persegi. Aturan pewarisan terpecah dengan cara yang sama: MediaBox dan CropBox dicari sepanjang rantai /Parent, sedangkan Bleed, Trim, dan ArtBox hanya dihitung kalau mereka duduk di dictionary halaman itu sendiri, jadi /TrimBox nyasar di node /Pages diperlakukan absen dan tak pernah disalin ke halaman

procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
  L, B, R, T: Single;
begin
  Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
  if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
    Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
  // True bahkan tanpa key /TrimBox: nilainya mundur ke CropBox, lalu MediaBox
  if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
    Writeln(Format('TrimBox  [%g %g %g %g]', [L, B, R, T]));
  // Preset Letter; GetLoadedPageVisibleBox membiarkan output tak tersentuh saat gagal
  L := 0; B := 0; R := 612; T := 792;
  Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
  Writeln(Format('Visible  [%g %g %g %g]', [L, B, R, T]));
end;

Jalankan helper itu sebelum dan sesudah flattening dan angka-angkanya menjelaskan dirinya sendiri. Untuk halaman 90 derajat dengan MediaBox [0 0 612 792], MediaBox hasil flattening menjadi [0 0 792 612]; setiap box yang ditulis ulang dipetakan lewat putaran searah jarum jam yang sama, relatif terhadap origin MediaBox asli, jadi MediaBox yang baru selalu mulai dari origin dan box lain mempertahankan posisinya di dalamnya. GetLoadedPageVisibleBox mengembalikan apa yang ditampilkan viewer dan dicetak printer, yaitu CropBox yang di-clip ke MediaBox dan dinormalkan agar Left lebih kecil dari Right, dan renderer, export SVG, viewer, serta jalur cetak HotPDF semuanya memakai box yang sama itu. Ketika Anda butuh ukuran halaman yang dilihat manusia, panggil GetLoadedPageVisibleBox alih-alih membaca /MediaBox

Kenapa HotPDF membaca setiap page box sebelum menulis apa pun saat FlattenLoadedPageRotation: BleedBox, TrimBox dan ArtBox default ke CropBox, yang sendirinya mundur ke MediaBox, sehingga memutar box satu per satu membuat CropBox membaca MediaBox yang sudah ditulis ulang dan putaran kedua menulis box yang tak pernah dimiliki halaman, memotong halaman landscape menjadi persegi
Rantai default berarti output satu box adalah input box lain — baca semuanya lebih dulu, transform terhadap origin MediaBox asli, lalu tulis

Kenapa anotasi rusak kalau hanya /Rect yang diputar?

Anotasi rusak karena appearance stream tak digambar langsung ke /Rect. Di bawah §12.5.5, viewer lebih dulu mentransform /BBox form dengan /Matrix-nya, lalu menskalakan dan mentranslasi bounding box dari hasil itu ke /Rect. Putar hanya /Rect, dan stamp 200 × 40 terjepit ke slot 40 × 200, tak terbaca dan berbaring menyamping. FlattenLoadedPageRotation karena itu mengalikan kanan putaran searah jarum jam halaman ke setiap /Matrix appearance (untuk 90 derajat, [0 -1 1 0 0 0] dalam konvensi vektor baris), di seluruh appearance /N, /R, dan /D beserta setiap state di dalamnya. Satu appearance stream bisa dipakai bersama oleh beberapa anotasi atau state, jadi setiap stream diputar tepat sekali per panggilan. Satu kasus tanpa jawaban bersih adalah stream yang dipakai bersama lintas halaman dengan rotasi berbeda; ia mengikuti halaman pertama yang mencapainya

Dua aturan lagi menjaga form field dan sticky note tetap di tempatnya. Entri /MK /R milik widget (§12.5.6.19) adalah sudut berlawanan arah jarum jam, jadi sudut searah jarum jam milik halaman dikurangkan darinya, modulo 360; lewati itu dan regenerasi appearance berikutnya menggambar teks field ke arah yang salah. Anotasi dengan flag NoRotate (posisi bit 5, nilai 16, §12.5.3) tetap tegak di halaman terotasi dan berporos pada sudut kiri atas /Rect-nya, jadi flattening mempertahankan lebar, tinggi, dan penampilan tegaknya, serta hanya memindahkan sudut itu ke tempat putaran menaruhnya. Di luar anotasi, method ini juga memutar /QuadPoints, /Vertices, /L, dan /InkList, menulis ulang destination eksplisit yang menyebut halaman (titik /XYZ, rectangle /FitR, dan /FitH / /FitV yang bertukar pada 90 dan 270 derajat, §12.3.2.2), dan mentransform geometri bertag seperti entri /BBox atribut untuk elemen struktur yang /Pg-nya halaman itu

Kenapa anotasi rusak saat halaman HotPDF diratakan hanya dengan memutar /Rect: stamp 200 kali 40 diskalakan ke slot 40 kali 200 dan jadi tak terbaca, sehingga FlattenLoadedPageRotation mengalikan kanan putaran searah jarum jam ke setiap /Matrix appearance di /N, /R dan /D, menyetel ulang /MK /R yang berlawanan jarum jam, dan memporoskan anotasi NoRotate pada sudut kiri atasnya
Viewer memasukkan BBox hasil transform appearance ke /Rect, jadi stream-nya sendiri yang harus diputar — satu pass per appearance bersama, tepat sekali per panggilan

Apa yang tak dicakup flattening?

Flattening adalah penulisan ulang geometris atas objek milik satu halaman, dan beberapa situasi jatuh di luarnya secara senyap, bukan gaduh

  • Halaman yang rotasi efektifnya sudah 0, atau yang MediaBox-nya hilang atau berlebar atau tinggi nol, dilewati tanpa error; bandingkan return value dengan jumlah halaman yang Anda harapkan berubah
  • Form XObject yang direferensikan dari resource halaman mempertahankan /BBox mereka sendiri di form space, karena cm terluar sudah memutarnya; scan structure tree hanya mengikuti /K dan /A sehingga tak pernah masuk ke resource atau anotasi halaman untuk kedua kalinya
  • Destination ditemukan dengan memindai setiap objek tak langsung sekali per halaman yang diratakan, jadi dokumen besar dengan ratusan halaman terotasi membayar penelusuran itu untuk masing-masing
  • Page renderer HotPDF tak menggambar anotasi, jadi pemeriksaan visual stamp yang diputar butuh FlattenLoadedAnnotations lebih dulu
// Panggang appearance ke konten supaya renderer bisa menampilkannya,
// lalu render halaman 1 sebelum dan sesudah /Rotate-nya dicabut
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
  Pdf.FlattenLoadedPageRotation('1', Info);
  After := Pdf.RenderLoadedPageToBitmap(0, 96);
  try
    Assert((Before.Width = After.Width) and (Before.Height = After.Height));
    // Bandingkan piksel penanda sudut di sini, bukan sekadar dimensinya
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

Untuk latar yang lebih dalam, sisi anotasi dari cerita ini berlanjut di menyintesis appearance anotasi sebelum diratakan, renderer di balik perbandingan sebelum-sesudah dibahas di merender halaman PDF termuat ke bitmap, dan redaksi dan N-up stitching pada PDF termuat memperlihatkan teknik append content stream yang sama yang diandalkan prefiks dan sufiks rotasi. HotPDF, termasuk FlattenLoadedPageRotation dan reader page box, tersedia untuk Delphi dan C++Builder di halaman HotPDF Delphi PDF component