Artikel Teknis

Fungsi PDF Type 2/3/4 di Delphi: Eksponensial, PostScript

HotPDF, komponen PDF VCL native untuk Delphi dan C++Builder, mengevaluasi tiga tipe fungsi PDF yang dibangun dari rumus alih-alih grid sampel: interpolasi eksponensial Type 2, stitching Type 3, dan fungsi kalkulator PostScript Type 4, sesuai ISO 32000-1 §7.10.3, §7.10.4, dan §7.10.5. Type 2 mencampur di antara dua vektor output sepanjang sebuah kurva, Type 3 merangkai beberapa sub-fungsi di satu domain input, dan Type 4 menjalankan program PostScript terbatas yang bisa bercabang, membandingkan, dan menghitung hampir apa saja yang dibutuhkan sebuah content stream dari inputnya. Salahkan sedikit saja salah satu dari ketiganya dan kegagalannya tidak pernah mengumumkan dirinya sebagai bug — ia muncul sebagai gradient dengan pita datar mati, warna spot yang dirender hitam pekat, atau fungsi kalkulator yang meleset tepat satu angka pada input yang kebetulan tidak dicoba sebuah test suite

Ketiganya berdampingan dengan yang keempat, Type 0, yang menyimpan grid tersampel alih-alih rumus dan dibahas terpisah di artikel pendamping tentang tabel lookup warna Type 0. Kedua keluarga ini menyelesaikan masalah yang sama, memetakan sebuah input ke sebuah output, tetapi Type 0 adalah data yang dihitung sekali dan dipanggang ke dalam file, sementara Type 2, 3, dan 4 adalah kode yang dievaluasi reader pada setiap pemanggilan. Keempatnya berbagi satu titik dispatch di renderer HotPDF, dikunci berdasarkan entry /FunctionType milik dictionary fungsi, sehingga sebuah shading, tint transform, atau fungsi spot halftone tidak pernah perlu tahu yang mana dari keempatnya yang diterimanya sebelum bisa meminta sebuah warna

Bagaimana cara kerja fungsi eksponensial PDF Type 2?

Sebuah fungsi PDF Type 2 menghitung satu rumus — y = C0 + x^N × (C1 − C0), diterapkan komponen demi komponen — di mana x adalah input tunggal fungsi tersebut, dinormalisasi terhadap /Domain-nya sebelum rumus berjalan (ISO 32000-1 §7.10.3). /C0 dan /C1 adalah vektor output di kedua ujung rentang itu, satu angka per komponen output, dan /N adalah eksponen yang membentuk kurva di antara keduanya: N = 1 memberikan ramp linear lurus di balik kebanyakan gradient stop dan konversi duotone, N di atas 1 menarik kurva ke arah C0, dan N di antara 0 dan 1 mendorongnya ke arah C1. RegisterExponentialFunction membangun dictionary itu dari lima argumen dan menyerahkan kembali sebuah objek fungsi yang siap dicolokkan ke sebuah shading, fungsi spot halftone, atau di mana pun spesifikasi menerima sebuah key /Function

Hubungan jumlah-komponen antara C0 dan C1 penting dua kali: sekali saat Anda menulis sebuah fungsi Type 2, dan sekali lagi kapan pun HotPDF harus merender fungsi yang tidak dibuatnya sendiri. Di sisi penulisan, RegisterExponentialFunction memeriksa C0 dan C1 satu sama lain dan memunculkan exception jika keduanya tidak sepakat, sehingga sebuah pemanggilan yang sampai ke BeginDoc sudah memiliki objek fungsi yang konsisten dengan dirinya sendiri. Di sisi rendering, meski begitu, evaluator harus memercayai apa pun array /C0 dan /C1 yang benar-benar dideklarasikan sebuah file sumber — sebuah file percetakan yang dibuka untuk pratinjau, misalnya, atau sebuah dokumen yang ditandatangani dan ditampilkan kembali ke pengguna — dan versi sebelum 2.376.0 membaca array tersebut ke dalam sebuah buffer berukuran empat komponen, kasus CMYK. Sebuah tint eksponensial DeviceGray atau DeviceRGB, dengan /C0 dan /C1 satu- atau tiga-elemen, gagal pada pembacaan itu secara diam-diam dan meninggalkan kedua array pada nol, sehingga tint tersebut dicat hitam pekat alih-alih warna yang dimaksudkan. Versi 2.376.0 mengubah ukuran reader menjadi sesuai jumlah output yang benar-benar dideklarasikan fungsi itu alih-alih buffer tetap — persis jenis bug yang hanya terungkap oleh test case non-CMYK, karena suite yang ada berjalan CMYK sepanjang waktu, di mana empat-ke-empat selalu cocok

var
  EaseIn: THPDFDictionaryObject;
begin
  // Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
  EaseIn := Pdf.RegisterExponentialFunction(
    [0,1],           // Domain: single input, clamped to [0,1]
    [0, 0, 0],       // C0: output at x = 0
    [0.8, 0, 0],     // C1: output at x = 1
    3,               // N: exponent, 1 = linear, > 1 eases toward C0
    []);             // Range omitted: defaults to a [0,1] clamp per output
end;

Stitching Type 3: merangkai sub-fungsi lewat array Bounds

Sebuah fungsi PDF Type 3 merangkai k sub-fungsi menjadi satu pemetaan piecewise di atas /Domain satu input, dan dua array yang membuat ini bekerja adalah /Bounds dan /Encode (ISO 32000-1 §7.10.4). /Bounds memegang k − 1 titik pisah interior yang membelah /Domain menjadi k interval berurutan; evaluator memilih interval pertama yang batas atasnya melebihi input, atau interval terakhir begitu input mencapai batas final, dan menyerahkan ke sub-fungsi interval itu. /Encode kemudian memetakan ulang input dari posisinya di dalam interval itu ke rentang input apa pun yang diharapkan sub-fungsi yang dipilih itu sendiri — biasanya [0, 1] jika sub-fungsi tersebut adalah satu segmen eksponensial lagi — sebelum evaluasi berlanjut, satu pemanggilan lebih dalam, ke /Domain dan /Range milik sub-fungsi itu sendiri

Evaluator stitching milik HotPDF dulunya hanya menangani tepat dua sub-fungsi, dan reader /Bounds-nya membutuhkan array delapan-elemen penuh, sehingga satu titik pisah yang sebenarnya dibutuhkan sebuah gradient dua-segmen — satu angka di /Bounds — selalu gagal di-parse dan fungsi itu tidak mengembalikan apa pun. /Encode sama sekali tidak diterapkan. Versi 2.376.0 menulis ulang pemilihan itu sebagai pencarian k-sub-fungsi umum yang dijelaskan spesifikasi dan mulai membaca /Bounds sesuai panjang aslinya yang dideklarasikan, sehingga sebuah gradient tiga-, empat-, atau lima-stop yang dirangkai dari sekian banyak segmen eksponensial sekarang terselesaikan dengan cara yang sama seperti yang selalu diklaim dilakukan gradient dua-segmen. Contoh di bawah membangun sebuah ramp dua-segmen hitam-melalui-merah-ke-putih, bentuk yang dicari sebuah shading axial atau radial kapan pun satu kurva eksponensial tidak bisa membawa setiap warna stop yang diminta sebuah desain

var
  ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
  // Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
  ToRed   := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0],   [0.8, 0, 0], 1, []);
  ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1],   1, []);

  Ramp := Pdf.RegisterStitchingFunction(
    [0,1],                  // Domain: the stitched function's own input range
    [ToRed, ToWhite],       // Functions: k = 2 sub-functions
    [0.5],                  // Bounds: k - 1 = 1 split point
    [0,1, 0,1],             // Encode: 2 numbers per sub-function
    []);                    // Range omitted: inherited from each sub-function
end;

Apa yang bisa dilakukan fungsi kalkulator PostScript Type 4 yang tidak bisa dilakukan Type 2 dan 3?

Sebuah fungsi PDF Type 4 menjalankan sebuah program sungguhan, meski sengaja dibatasi: sebuah kalkulator PostScript yang mendorong inputnya ke sebuah operand stack, mengeksekusi operator aritmetika, perbandingan, manipulasi stack, dan boolean plus kondisional if/ifelse, dan meninggalkan outputnya di stack ketika selesai (ISO 32000-1 §7.10.5, Table 42). Tidak ada konstruksi loop dan tidak ada penyimpanan variabel bernama, hanya stack, yang membuat sebuah program yang sesuai standar mudah dinalar — tetapi di dalam set operator terbatas itu, Type 4 bisa mengekspresikan hal-hal yang tidak bisa dilakukan Type 2 dan Type 3, seperti rumus pencampuran multi-ink sungguhan untuk sebuah separation DeviceN atau fungsi spot halftone dengan threshold kondisional. Evaluator milik HotPDF, HPDFEvalPostScriptCalculator, men-tokenize program itu sekali — angka, operator, dan blok prosedur { } — lalu menelusuri sebuah operand stack 100-entri, kedalaman yang diminta ISO 32000-1 §7.10.5, di balik batas keras 50.000 operator yang dievaluasi sebagai pengaman defensif terhadap program yang patologis atau ditulis tangan

Operator roll: arahnya mudah terbalik

roll adalah operator yang paling mungkin keluar terbalik pada percobaan pertama, karena urutan argumen dan arah rotasinya sama-sama berlawanan dengan cara bahasa Inggris mendeskripsikannya. n j roll mem-pop sebuah hitungan n dan sebuah jumlah rotasi j, lalu menggeser secara siklis n entri teratas stack sejauh j posisi, membungkus item yang jatuh dari satu ujung kembali ke ujung lainnya; contoh kanonik, langsung dari spesifikasi, adalah a b c 3 1 roll menghasilkan c a b — item teratas berpindah ke bawah kelompok, bukan sebaliknya, dan setiap item lain bergeser naik satu untuk memberi ruang. Evaluator milik HotPDF menghitung posisi baru entri stack i sebagai (i + j) mod n, yang cocok persis dengan contoh itu, tetapi ini adalah loop dua-baris yang sama mudahnya ditulis dengan rotasi terbalik, dan sebuah roll yang tercermin tetap menghasilkan warna yang terlihat masuk akal — hanya saja bukan warna yang diminta penulis file

const
  Prog = '{ 3 1 roll }';   // (a b c) -> (c a b): the third input moves to the front
var
  Reorder: THPDFStreamObject;
begin
  // Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
  Reorder := Pdf.RegisterPostScriptFunction(
    [0,1, 0,1, 0,1],   // Domain: 2 numbers per input
    [0,1, 0,1, 0,1],   // Range: 2 numbers per output (required for Type 4)
    Prog);
end;

round bukan Round milik Delphi: pembulatan setengah-ke-atas versus pembulatan bankir

Operator round milik PostScript menyelesaikan seri seri .5 ke arah bilangan bulat yang lebih besar setiap saat, dan fungsi Round bawaan Delphi tidak begitu: fungsi ini membulatkan setengah-ke-genap, konvensi pembulatan bankir yang mengubah-ubah ke arah mana seri .5 jatuh sehingga pembulatan berulang tidak mengakumulasi bias. Keduanya sepakat di hampir semua tempat dan tidak sepakat persis pada batas yang penting di sini — Round(0.5) milik Delphi mengembalikan 0 dan Round(2.5) mengembalikan 2, sementara round milik spesifikasi PDF menginginkan 1 dan 3 untuk input yang sama itu — sehingga ketidakcocokan ini bersembunyi lewat pengujian biasa dan kemudian muncul kembali sebagai selisih-satu yang konsisten di mana pun aritmetika perantara sebuah program kalkulator mendarat tepat di setengah-bilangan-bulat. ISO 32000-1 §7.10.5 Table 42 eksplisit bahwa round mendorong pecahan .5 ke arah bilangan bulat yang lebih besar, sehingga HotPDF mengimplementasikan operator itu sebagai Floor(x + 0.5) alih-alih memanggil Round milik Delphi, dan kode apa pun yang mengimplementasikan ulang atau memeriksa spot aritmetika sebuah program Type 4 secara manual membutuhkan substitusi yang sama

function PostScriptRound(const X: Double): Double;
begin
  // ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
  // integer. Delphi's Round() is banker's rounding and disagrees here:
  // Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
  Result := Floor(X + 0.5);
end;

Validasi saat registrasi menangkap program kalkulator yang buruk lebih awal

Sebuah program Type 4 yang cacat murah untuk ditangkap saat waktu penulisan dan mahal untuk ditangkap di tempat lain mana pun, sehingga RegisterPostScriptFunction tidak sekadar menyimpan teks sumbernya: ia mengevaluasi-percobaan program itu sekali, pada titik tengah /Domain yang dideklarasikan, sebelum objek fungsi itu pernah ditulis ke dalam dokumen. Blok { } yang tidak seimbang, sebuah operator yang tidak dikenali, stack underflow, atau jumlah output yang tidak cocok dengan /Range semuanya gagal pada percobaan itu dan langsung memunculkan exception, dengan call stack menunjuk ke pemanggilan RegisterPostScriptFunction alih-alih ke sebuah artefak rendering yang ditemukan selama QA pada sebuah file yang sudah dirilis. Percobaan titik-tengah itu tidak membuktikan program tersebut benar di seluruh /Domain-nya — sebuah cabang kondisional yang hanya berperilaku salah di dekat satu tepi rentang input masih bisa lolos dari satu titik sampel — tetapi ia menutup seluruh kelas program yang secara struktural rusak alih-alih sekadar salah di satu sudut

Di mana gradient dan warna spot mempekerjakan fungsi-fungsi ini

Type 2, 3, dan 4 jarang muncul sendirian dalam sebuah PDF sungguhan; mereka muncul di mana pun spesifikasi menerima sebuah key /Function, dan dua konsumen paling umum adalah shading dan tint transform warna spot. Operator sh milik sebuah gradient axial atau radial (ISO 32000-1 §8.7.4.5) mengevaluasi /Function-nya sekali per posisi sepanjang sumbu gradient, yang persis merupakan kasus multi-stop yang menjadi alasan keberadaan stitching Type 3. Tint transform ruang warna Separation atau DeviceN adalah tempat sering lainnya untuk ketiga tipe ini, dan di situlah Type 4 membuktikan gunanya: sebuah tinta spot tunggal biasanya tereduksi menjadi sebuah kurva Type 2 atau Type 0, tetapi sebuah campuran DeviceN dari beberapa tinta dengan perilaku trapping dan overprint sungguhan seringkali membutuhkan logika kondisional yang hanya bisa diekspresikan sebuah kalkulator PostScript, kasus yang dibahas di artikel tentang merender warna spot Separation dan DeviceN. RegisterSeparationFunc adalah pemanggilan pasangannya di sisi penulisan: ia menerima sebuah nama colorant, sebuah ruang warna alternatif, dan objek apa pun yang dikembalikan keluarga Register*Function, lalu menghubungkan tint transform itu ke sebuah resource ruang warna Separation yang bisa dipilih sisa halaman dengan scn/SCN

Bersama-sama, grid tersampel Type 0 dan ketiga tipe berbasis-rumus ini mencakup setiap /Function yang bisa dideklarasikan sebuah PDF, dan memilih yang tepat sebagian besar merupakan pertanyaan tentang apa yang sudah Anda miliki: sebuah tabel lookup yang dihitung di tempat lain menjadi Type 0, sebuah campuran dua-titik-akhir menjadi Type 2, beberapa campuran yang dirangkai di satu domain menjadi Type 3, dan apa pun dengan logika kondisional sungguhan menjadi Type 4. RegisterExponentialFunction, RegisterStitchingFunction, dan RegisterPostScriptFunction adalah bagian dari HotPDF Component standar untuk Delphi dan C++Builder, berdampingan dengan sisa API fungsi dan shading ISO 32000-1 miliknya