Artikel Teknis

Advance Teks PDF dan Restore Clip q/Q di Renderer Delphi

Renderer halaman milik HotPDF Delphi Component kini memajukan teks dengan menghitung perpindahan setiap glyph di text space, tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th sebagaimana didefinisikan ISO 32000-1 §9.4.4, lalu memindahkan text matrix melalui bagian linearnya dengan HPDFTranslateTextMatrix. Clipping disimpan per frame q dan dipulihkan pada Q, tapi region GDI hanya ditangkap ketika frame itu benar-benar mengubah clip-nya. Kedua perbaikan mendarat di HotPDF 2.754.0, dan keduanya datang dari halaman dunia nyata yang terrender dengan kata-kata menggumpal atau region clip yang bocor melewati Q-nya. Bug pertama adalah aritmetika yang tampak benar sampai ada produser yang menulis ukuran fontnya ke dalam matriks. Yang kedua adalah perbaikan kebenaran yang nyaris mencabut percepatan render paralel kami, dan cara kami mendapatkan kembali kecepatannya layak diketahui kalau Anda menulis perangkat PDF apa pun berbasis GDI

Kenapa teks menggumpal saat sebuah PDF memakai Tf 1?

Karena kode advance yang lama menambahkan jarak text-space langsung ke komponen translasi Tm, seolah-olah text space dan user space selalu berskala sama. Banyak produser dunia nyata menyetel ukuran font ke 1 dengan Tf dan membawa ukuran sebenarnya di text matrix. Dengan /F1 1 Tf dan 12 0 0 12 72 700 Tm, glyph selebar 500 unit bergerak maju 0,5 di text space, yang menjadi 6 titik di halaman begitu Tm menskalakannya. Renderer lama menjalankan Tm.e := Tm.e + Adv dan menggerakkan pena 0,5 titik. Setiap glyph mendarat seperdua belas karakter setelah pendahulunya, jadi satu baris teks badan terrender sebagai noda gelap di margin kiri sementara file yang sama tampak sempurna di viewer lain mana pun

Kenapa teks menggumpal di bawah Tf 1 di renderer HotPDF: dengan 12 0 0 12 72 700 Tm, glyph 500 unit harus bergerak maju 0,5 unit text-space yang diskalakan Tm menjadi 6 titik, sementara kode lama menambahkan 0,5 langsung ke Tm.e dan merender satu baris teks badan sebagai noda seperdua belas karakter per glyph
Produser yang mengenkode ukuran font di text matrix membuat setiap glyph mendarat seperdua belas karakter setelah pendahulunya, cacat yang tak terlihat pada output milik library sendiri
// Content stream dari produser yang mengenkode ukuran di Tm, bukan Tf:
//   BT
//   /F1 1 Tf
//   12 0 0 12 72 700 Tm
//   [(Hel) 30 (lo) -250 (world)] TJ
//   ET

// Advance lama (disederhanakan): jarak ditambahkan ke Tm.e seolah user space
Adv := W * FontSize / 1000;                  // 0,5 untuk glyph 500 unit
if (HorizScale <> 0) and (HorizScale <> 100) then
  Adv := Adv * HorizScale / 100;             // Th pada lebarnya saja
Adv := Adv + CharSpace;                      // Tc tak diskalakan Th
if Code = 32 then
  Adv := Adv + WordSpace * FontSize / 1000;  // Tw salah diskalakan Tfs
Tm.e := Tm.e + Adv;                          // mengabaikan Tm.a, Tm.b, Tm.c, Tm.d

// Penyesuaian TJ lama: tanpa Th, dan lagi-lagi hanya Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;

Jalan pintas Tm.e bukan satu-satunya cacat di blok itu. Word spacing Tw dinyatakan dalam unit text space tak terskala, tapi kode lama mengalikannya dengan FontSize / 1000, jadi di bawah Tf 12 satu baris justified kehilangan hampir seluruh celah antarkatanya. Penskalaan horizontal Th diterapkan pada lebar glyph tapi tidak pada Tc atau Tw, dan penyesuaian kerning TJ melewatinya sepenuhnya. Jalur non-painting yang memajukan teks tak terlihat render mode 3 — jenis yang dipakai text layer OCR — dan teks di dalam optional content tersembunyi membawa salinan privat dari aritmetika yang sama, jadi apa pun yang digambar setelah satu run tak terlihat dimulai dari posisi yang salah. Bug text state di sebuah renderer jarang gagal dengan gaduh: seperti bug indeks operand dan nama resource yang pernah menolkan Tc, Tw, dan Tz tanpa satu error pun, ini menghasilkan halaman yang tampak masuk akal pada output milik library sendiri dan hanya rusak pada file dari produser lain

Bagaimana ISO 32000-1 §9.4.4 mendefinisikan advance glyph?

ISO 32000-1 §9.4.4 mendefinisikan advance sepenuhnya di text space dan menerapkannya ke text matrix sebagai matriks translasi, jadi jawabannya adalah menghitung tx lebih dulu dan membiarkan Tm mengerjakan penskalaan, rotasi, dan skew. Untuk penulisan horizontal, tx sama dengan ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th, di mana w0 adalah lebar glyph dalam seperseribu em, Tj adalah penyesuaian TJ, dan Th adalah Tz dibagi 100. Tm yang baru adalah [1 0 0 1 tx 0] × Tm, yang di HotPDF adalah helper HPDFTranslateTextMatrix: ia menambahkan X dan Y melalui koefisien matriks a, b, c, dan d alih-alih menulis ke e dan f secara langsung. Menurut §9.3.3, Tw hanya berlaku untuk kode karakter single-byte 32, jadi kode CID multibyte tak pernah kebagian word spacing di jalur horizontal. Helper yang sama kini menggerakkan Td, TD, T*, operator ' dan ", penyesuaian TJ, serta jalur teks tersembunyi, artinya satu fungsi memiliki aturannya

Advance glyph ISO 32000-1 9.4.4 di renderer HotPDF: tx dihitung di text space dari w0, Tj, Tfs, Tc, Tw dan Th, lalu diterapkan lewat HPDFTranslateTextMatrix sehingga perpindahannya melewati koefisien matriks a, b, c dan d, serta Td, TD, TJ dan jalur teks tersembunyi berbagi satu aturan
Menambahkan advance ke Tm.e langsung hanya bekerja ketika text space sama dengan user space; melewatinya lewat koefisien matriks menjaga teks terskala, terotasi, dan terskew tetap benar
procedure HPDFTranslateTextMatrix(var Matrix: THPDFAffineMatrix; X, Y: Double);
begin
  Matrix.e := Matrix.e + Matrix.a * X + Matrix.c * Y;
  Matrix.f := Matrix.f + Matrix.b * X + Matrix.d * Y;
end;

// Advance glyph horizontal, ISO 32000-1 9.4.4
W   := HPDFFontCharWidth(F, Code);
Adv := W * State.Text.FontSize / 1000 + State.Text.CharSpace;
if (Code = 32) and not F.CID2Byte then
  Adv := Adv + State.Text.WordSpace;         // Tw di text space, tanpa skala
Adv := Adv * State.Text.HorizScale / 100;    // Th berlaku untuk seluruh jumlahnya
HPDFTranslateTextMatrix(Tm, Adv, 0);

// Elemen angka TJ: space yang sama, Th yang sama
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);

Penempatan glyph harus mengikuti logika yang sama. Ketika tak ada outline tertanam yang tersedia dan renderer mundur ke TextOutW GDI, ia kini membangun matriks glyph penuh dari CTM × Tm × rise × skala em, termasuk Th, dan memasangnya dengan SetWorldTransform dalam mode GM_ADVANCED di dalam sepasang SaveDC / RestoreDC. Font GDI dibuat pada tinggi 1000 unit yang tetap dan transform itulah yang mengurus ukurannya, jadi teks terotasi dan terskew mempertahankan orientasinya alih-alih digambar tegak di titik origin yang ditransform. Mode penulisan vertikal adalah satu asimetri yang disengaja: font WMode 1 bergerak maju ke bawah sumbu y menurut metrik vertikalnya, dan penskalaan horizontal tidak berlaku pada sumbu itu

Apa yang sebenarnya disimpan q/Q dalam graphics state PDF?

ISO 32000-1 §8.4.2 mendaftarkan clipping path saat ini sebagai bagian dari graphics state, jadi Q harus memulihkan clip persis seperti pada q yang bersesuaian, bukan hanya parameter numeriknya. HotPDF sudah menyimpan stack graphics state dengan CTM, warna, parameter garis, dan text state, tapi GDI menyimpan clip di device context, di luar stack itu. Salinan state numerik karena itu memulihkan segalanya kecuali clip, dan clip yang dipasang dengan W n di dalam blok q ... Q terus memotong setiap operasi berikutnya di halaman. Form XObject menambah jalur kedua menuju kegagalan yang sama, karena §8.10 memberi sebuah form save dan restore implisit di sekeliling kontennya, dan konten form dunia nyata kadang meninggalkan operator q miliknya sendiri tak berpasangan meski spesifikasi mensyaratkan mereka berpasangan. Renderer kini memanggil CaptureClipBeforeChange dan SaveDC sebelum menjalankan sebuah form, lalu setelah form selesai ia membuang region tersimpan mana pun yang lebih dalam dari kedalaman masuknya dan memanggil RestoreDC, jadi setiap HRGN tersimpan punya tepat satu jalur pelepasan

Penangkapan clip lazy dengan THPDFSavedClipState

Perbaikan yang dirilis menyimpan satu record THPDFSavedClipState per q, tapi menunda bagian mahalnya sampai frame itu pertama kali mengubah clip. Record itu memegang handle region, kedalaman stack tempatnya bernaung, device context tempat ia diambil, dan flag Captured. DevPushState hanya mengisi kedalaman dan DC serta menumbuhkan array frame dengan penggandaan dari 16, jadi content stream yang penuh q 1 0 0 1 x y cm ... Q tak mengalokasikan objek GDI sama sekali. Operator yang hendak mengubah clipping — maksudnya path painting dengan W atau W* tertunda, operator n, pengisian pattern, dan masuk form — memanggil CaptureClipBeforeChange lebih dulu

Penangkapan clip GDI lazy di renderer HotPDF: DevPushState hanya mencatat kedalaman dan DC per q, CaptureClipBeforeChange membaca region tepat sebelum W, n atau masuk form mengubah clipping, DevPopState memulihkan dan menghapusnya pada Q, dan versi eager yang menangkap pada setiap q menjatuhkan throughput paralel ke sekitar 1,15 kali single-threaded
Membuat region GDI pada setiap q membuat thread render kelaparan, jadi penangkapan kini hanya terjadi ketika sebuah operator hendak mengubah clipping dan gate percepatan 1,5 kali lolos lagi
procedure THPDFPageRenderer.CaptureClipBeforeChange;
var
  Index, ClipResult: Integer;
  Region: HRGN;
begin
  ClearSavedClipRegions(FGSStack.Count);
  Index := FSavedClipCount - 1;
  if (Index < 0) or FSavedClips[Index].Captured or
     (FSavedClips[Index].StackDepth <> FGSStack.Count) or
     (FSavedClips[Index].DC <> FDC) then Exit;   // sudah tersimpan, atau bukan milik kita
  Region := CreateRectRgn(0, 0, 0, 0);
  if Region = 0 then RaiseLastOSError;
  ClipResult := GetClipRgn(FDC, Region);         // 0 berarti tak ada clip sama sekali
  if ClipResult <= 0 then
  begin
    DeleteObject(Region);
    Region := 0;
    if ClipResult < 0 then RaiseLastOSError;
  end;
  FSavedClips[Index].Region := Region;
  FSavedClips[Index].Captured := True;
end;

procedure THPDFPageRenderer.DevPopState;
var
  Index: Integer;
begin
  ClearSavedClipRegions(FGSStack.Count);
  Index := FSavedClipCount - 1;
  if (Index >= 0) and (FSavedClips[Index].StackDepth = FGSStack.Count) then
  begin
    if FSavedClips[Index].Captured and (FSavedClips[Index].DC = FDC) then
      SelectClipRgn(FDC, FSavedClips[Index].Region);  // Region 0 mencabut clip
    if FSavedClips[Index].Region <> 0 then
      DeleteObject(FSavedClips[Index].Region);
    Dec(FSavedClipCount);
  end;
  FGSStack.Pop;
end;

Biaya terukur versi eager itulah alasan desain ini ada. Implementasi benar yang pertama membuat dan membaca sebuah region GDI pada setiap q, dan pada halaman yang sebagian besar berisi transform numerik, thread renderer menghabiskan waktunya berebut objek region GDI alih-alih merasterisasi. Pipeline render paralel jatuh dari keuntungan yang diharapkan ke sekitar 1,13 sampai 1,20 kali throughput single-threaded dan gagal di gate percepatan 1,5 kali dalam suite benchmark-nya. Dengan penangkapan lazy dan kapasitas frame yang dipakai ulang, benchmark yang sama lolos di gate 1,5 kali yang asli lagi. Antialiasing glyph TrueType yang kecil mendarat di rilis yang sama dan menjadi tersangka yang jelas, tapi regresinya dilacak kembali ke alokasi region, dan itu pengingat yang bagus untuk mengukur sebelum menyalahkan fitur terbaru

Di mana batas-batas pendekatan ini?

Clip yang disimpan adalah region GDI dalam piksel perangkat, jadi ia persis untuk bitmap yang sedang dirender dan tak bermakna untuk target lain mana pun. Itulah kenapa setiap frame mencatat device context-nya dan DevPopState melewati pemulihan ketika DC sudah berubah, misalnya saat sebuah transparency group merender ke bitmap layer miliknya sendiri. GetClipRgn yang mengembalikan nol adalah hasil yang sah yang berarti tak ada clip, dan memulihkannya dengan SelectClipRgn(FDC, 0) itulah yang benar mencabut clip yang tak ada pada q yang bersesuaian. Di sisi teks, perbaikan ini mengoreksi ke mana setiap glyph pergi, tapi ia tidak mengarang lebar: kalau sebuah font menghilangkan array /Widths-nya dan program tertanamnya tak tersedia, advance-nya tetap hanya sebaik fallback lebarnya. Ketika Anda regression-test area ini, simpan setidaknya satu fixture dengan Tf 1 dan Tm terskala, satu dengan Tz dan Tw bukan nol, dan satu dengan clip di dalam q ... Q disusul konten di luarnya, karena tak satu pun dari itu muncul di dokumen yang dibuat oleh library ini sendiri

Kalau Anda menggerakkan renderer dari kode aplikasi, tak ada yang berubah pada pola pemanggilan yang dijelaskan di merender halaman PDF ke bitmap, dan halaman yang tadinya menampilkan garis bernoda atau konten terpotong seharusnya sekadar terrender dengan benar di 2.754.0 dan sesudahnya. Detail tentang komponennya, versi Delphi dan C++Builder yang didukung, serta lisensinya ada di halaman produk HotPDF Delphi PDF Component