Artikel Teknis

Cara Kerja Grafis PDF: Content Stream dan Operator

Halaman PDF tidak menyimpan piksel, dan tidak menyimpan pohon objek bentuk seperti yang dilakukan SVG. PDF menyimpan sebuah program. Setiap garis, kurva, isi, dan gambar yang ditempatkan di halaman merupakan hasil eksekusi urutan operator dalam sebuah content stream, dari atas ke bawah, terhadap graphics state yang sedang berjalan. Pahami satu fakta itu dan sebagian besar perilaku format berhenti mengejutkan: mengapa isian membutuhkan operator painting terpisah setelah path dibangun, mengapa warna dan lebar garis bocor dari satu bentuk ke bentuk berikutnya kecuali Anda membracketnya, mengapa kode gambar yang sama dapat mendarat di tempat yang sama sekali berbeda setelah satu transformasi koordinat. Ini adalah panduan tentang model eksekusi tersebut sebagaimana didefinisikan dalam ISO 32000: operator yang Anda temui saat membuka content stream, dan aturan yang menentukan apa yang muncul di halaman

Content stream adalah bytecode postfix

Content stream adalah urutan byte datar berupa operand diikuti operator. Operand datang lebih dulu, operator yang mengonsumsinya datang terakhir, yang merupakan kebalikan dari pemanggilan fungsi dan identik dengan mesin tumpukan: dorong angkanya, lalu keluarkan kata kerjanya. Tidak ada penumpukan, tidak ada sintaks ekspresi, tidak ada variabel. Garis besar segitiga hanya membutuhkan lima baris seperti ini:

100 100 m    % moveto: start a new subpath at (100, 100)
200 200 l    % lineto: add a segment to (200, 200)
300 100 l    % lineto: add a segment to (300, 100)
h            % closepath: connect back to the start
S            % stroke: paint the path outline

Operator tersebut sengaja dibuat singkat. Halaman nyata berisi ribuan baris ini, biasanya terkompresi dengan FlateDecode. Biaya dari keringkasan tersebut adalah bahwa stream tidak membawa struktur yang dapat Anda kueri: penampil tidak dapat bertanya "di mana judul di halaman ini," ia hanya dapat menjalankan program dan melihat tinta apa yang mendarat di sana. Itu adalah alasan mendasar mengapa ekstraksi teks dari PDF arbitrari itu sulit

Asal koordinat ada di kiri-bawah, dan Y tumbuh ke atas

Sebelum koordinat mana pun masuk akal, Anda harus tahu di mana (0, 0) berada. PDF menempatkan asal di sudut kiri-bawah halaman, dengan X meningkat ke kanan dan Y meningkat ke atas, diukur dalam poin dengan 72 poin per inci (ISO 32000-2 §8.3.2). Di halaman US Letter tepi atas berada di y = 792, bukan y = 0. Siapa pun yang datang dari grafis layar, di mana asal ada di kiri-atas dan Y tumbuh ke bawah, akan salah mengira ini pada percobaan pertama dan menggambar garis pertama di luar bawah halaman. Satuannya juga independen dari media: 72 unit adalah satu inci baik halaman dirender ke layar ponsel maupun ke imagesetter

Sebagian besar pustaka penggambaran halaman mewarisi konvensi ini secara langsung. Di HotPDF, misalnya, TextOut dan pemanggilan path semuanya diukur dari kiri-bawah dalam poin, sehingga nilai mendekati tinggi halaman menempatkan konten di bagian atas:

// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // emits the moveto/lineto/stroke operators

Urutan pemanggilan tersebut dikompilasi langsung menjadi operator m, l, dan S di atas. Pustaka adalah juru ketik untuk content stream, tidak lebih, dan mengetahui apa yang dipancarkannya adalah yang memungkinkan Anda memahami output saat sebuah bentuk mendarat di tempat yang tidak Anda harapkan

Bangun path-nya, lalu lukis

PDF memisahkan konstruksi path dari pengecatan path, dan pemisahan tersebut bukan sekadar formalitas. Anda pertama-tama mendeskripsikan sebuah bentuk dengan operator konstruksi yang tidak menambahkan apa pun yang terlihat, lalu mengeluarkan satu operator pengecatan yang memutuskan apa yang harus dilakukan dengan path yang terakumulasi. Segitiga yang sama dapat berupa garis besar, isian solid, atau keduanya, tergantung hanya pada kata kerja yang Anda akhiri dengannya

Operator konstruksi jumlahnya sedikit. m memulai subpath baru di sebuah titik. l menambahkan segmen lurus. c menambahkan kurva Bezier kubik dari enam operand, dua titik kontrol dan satu titik akhir. re adalah pintasan yang menambahkan seluruh persegi panjang dari kuadruplet x, y, lebar, tinggi. h menutup subpath saat ini kembali ke awalnya. Tidak satu pun dari operator ini yang meletakkan tinta di halaman; mereka hanya mengakumulasi geometri

200 250 m                    % start the subpath
300 350 400 450 500 250 c    % cubic Bezier: two control points, then endpoint
150 200 re                   % a 150 x 200 rectangle, added as its own subpath
h                            % close

Contoh awal menggunakan varian operator kurva y yang sekarang sudah usang; c dengan tiga titik eksplisitnya adalah bentuk yang akan Anda lihat dalam praktik dan yang harus Anda gunakan. Setelah path ada, satu operator pengecatan menyelesaikannya. Kosakatanya kecil dan layak dihafalkan, karena setiap bentuk di setiap halaman berakhir dengan salah satu dari ini:

  • S menggoreskan garis besar path menggunakan lebar garis dan warna goresan saat ini
  • f mengisi bagian dalam menggunakan warna isian saat ini dan aturan liku nonzero
  • f* mengisi menggunakan aturan ganjil-genap, yang penting untuk bentuk yang berpotongan sendiri dan bentuk dengan lubang
  • B mengisi kemudian menggoreskan dalam satu operasi; b menutup path terlebih dahulu
  • n tidak melukis apa pun, yang merupakan cara sebuah path menjadi region klip tanpa meninggalkan tanda yang terlihat

Aturan liku adalah bagian yang sering disalahpahami. Nonzero (f, B) menghitung persilangan bertanda dari sinar dari titik uji dan mengisi di mana pun hitungannya tidak nol, sehingga lubang hanya tetap kosong jika subpath-nya meliku berlawanan arah dengan yang luar. Ganjil-genap (f*, B*) beralih di setiap persilangan terlepas dari arah. Jika bentuk "donat" keluar solid, lingkaran dalam diliku dengan arah yang sama dengan lingkaran luar, dan Anda harus membaliknya atau beralih ke ganjil-genap

Warna adalah mode, bukan parameter

Warna dalam content stream bersifat melekat. Anda mengatur warna dan warna itu tetap diatur sampai Anda mengatur warna lain atau memulihkan state sebelumnya, itulah mengapa perubahan warna yang tidak dibracket secara diam-diam mewarnai semua yang digambar setelahnya. PDF juga menyimpan warna isian dan warna goresan sebagai dua pengaturan independen, dengan operator huruf kecil untuk isian dan huruf besar untuk goresan. Ruang warna perangkat masing-masing memiliki singkatannya sendiri:

0.5 g                % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg       % DeviceRGB fill
0.8 0.2 0.1 RG       % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k    % DeviceCMYK fill

DeviceRGB cocok untuk output layar, DeviceCMYK adalah yang diharapkan produksi cetak, dan DeviceGray adalah pilihan terkecil untuk konten monokrom. Ruang perangkat itu mudah digunakan tetapi tidak dikalibrasi: triple RGB yang sama dapat dirender secara berbeda di dua monitor, yang merupakan masalah yang diselesaikan oleh ruang warna berbasis ICC dan output intent PDF/A. Untuk pekerjaan kritis-warna Anda memilih ruang yang dikalibrasi dengan cs dan CS dan mengatur komponen dengan sc dan scn, tetapi untuk dokumen biasa singkatan perangkat sudah mencukupi. Sebuah pustaka membungkus ini dalam panggilan bertipe. HotPDF, misalnya, mengambil satu TColor dan memancarkan operator yang sesuai:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, width, height
Pdf.CurrentPage.Fill;

Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50);           // x, y, radius
Pdf.CurrentPage.Fill;

Graphics state dan stack q/Q

Semua yang bukan path itu sendiri hidup dalam graphics state: matriks transformasi saat ini, warna isian dan goresan, lebar garis, pola putus-putus, region klip, alfa. State bersifat global dan bisa diubah, sehingga satu-satunya cara aman untuk membuat perubahan lokal adalah menyimpan semuanya, memodifikasinya, menggambar, dan mengembalikannya. Itulah yang dilakukan q dan Q. q mendorong salinan state saat ini ke sebuah tumpukan; Q memunculkannya, membuang setiap perubahan yang dibuat sejak q yang cocok

q                    % save the entire graphics state
2 0 0 2 100 100 cm   % concatenate a transform: scale 2x, translate to (100,100)
0.8 g                % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q                    % restore: transform and color revert

q dan Q yang tidak seimbang adalah cara umum content stream yang dibuat manual atau disambungkan menjadi salah. q yang tersesat tanpa Q yang cocok meninggalkan tumpukan dalam ketika halaman berakhir; Q ekstra mengalami underflow. Dalam kasus apa pun penampil dapat mempertahankan klip atau transformasi lama yang aktif, dan konten menghilang atau mendarat di tempat yang salah. Ketika grafis menghilang tanpa alasan yang dapat dijelaskan oleh path, audit tumpukan state terlebih dahulu

CTM mentransformasi setiap koordinat

Matriks transformasi saat ini berada di antara angka-angka dalam operator Anda dan halaman sebenarnya. Setiap koordinat dikalikan dengan CTM sebelum apa pun digambar, sehingga mengubah matriks mengubah di mana dan bagaimana semua gambar berikutnya muncul tanpa menyentuh satu koordinat path pun. Operator cm menggabungkan matriks baru ke yang saat ini, mengambil enam operand yang memetakan ke matriks affine [a b c d e f]:

1 0 0 1 100 50 cm        % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm         % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm   % rotate 45 degrees (cos/sin in a, b, c, d)

Dua hal yang sering menjebak orang. Pertama, cm menyusun bukan menggantikan, sehingga transformasi berakumulasi dan urutan penting: penskalaan lalu translasi tidak sama dengan translasi lalu penskalaan. Kedua, rotasi dan penskalaan berputar di sekitar asal saat ini, bukan pusat bentuk Anda, sehingga untuk memutar sesuatu di tempat Anda menerjemahkannya ke asal, memutar, lalu menerjemahkan kembali, semuanya dibungkus dalam q/Q. Matriks yang sama inilah yang menempatkan gambar, bagian terakhir yang layak dilihat

Gambar dan konten yang dapat digunakan ulang adalah XObject

Gambar raster tidak hidup secara inline dalam content stream. Mereka disimpan sebagai image XObject, objek eksternal dengan kamus mereka sendiri yang mendeskripsikan lebar, tinggi, kedalaman bit, ruang warna, dan filter kompresi, dan content stream hanya mereferensikannya. Foto berbasis JPEG mendeklarasikan dirinya seperti ini:

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % the image data is a JPEG stream
>>

Image XObject menggambar ke dalam persegi satuan: ia selalu menempati wilayah dari (0, 0) ke (1, 1) dalam ruang pengguna. Anda tidak meneruskan posisi atau ukuran kepadanya. Sebaliknya Anda mengatur CTM sehingga persegi satuan tersebut memetakan ke persegi panjang yang Anda inginkan, kemudian memanggilnya dengan Do. Itulah mengapa menempatkan gambar selalu merupakan transformasi diikuti pemanggilan, dibungkus dalam simpan/pulihkan agar skalanya tidak bocor ke operasi berikutnya:

q
640 0 0 480 50 300 cm    % map the unit square to a 640x480 box at (50, 300)
/Photo Do                % paint the image XObject
Q

Mekanisme Do yang sama menggerakkan form XObject, yang menampung potongan grafis yang dapat digunakan ulang, logo atau cap berulang, sebagai content stream mereka sendiri dengan bounding box. Definisikan sekali, panggil berkali-kali dengan CTM yang berbeda, dan byte tersebut hanya muncul sekali dalam file. Sebagian besar pustaka menyembunyikan ini di balik satu pemanggilan penempatan: HotPDF mendaftarkan bitmap dengan AddImage dan menempatkannya dengan ShowImage, mengambil x, y, lebar, dan tinggi eksplisit alih-alih meminta Anda membangun matriks secara manual:

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (bottom-left), width, height, rotation angle
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

Di balik satu baris itu pustaka menulis kamus image XObject, mengatur CTM untuk mengukur dan memposisikan persegi satuan, dan memancarkan Do. Model di baliknya adalah yang layak diketahui, karena menjelaskan setiap hasil aneh: gambar yang diregangkan adalah CTM dengan faktor skala yang tidak cocok, logo yang identik di empat puluh halaman adalah satu form XObject yang dipanggil empat puluh kali, dan gambar yang dirender terbalik adalah tanda negatif dalam matriks, bukan file yang rusak

Ke mana ini mengarah

Model grafis ini kecil begitu Anda melihat bentuknya. Content stream adalah bytecode postfix yang berjalan terhadap state yang bisa diubah; koordinat dimulai dari kiri-bawah dan melewati CTM; path dibangun secara diam-diam dan dilukis dengan satu operator yang disengaja; pengaturan warna dan garis bertahan sampai Anda membracketnya dengan q/Q; gambar dan grafis yang dapat digunakan ulang adalah XObject yang ditempatkan dengan mentransformasi persegi satuan. Hampir setiap hasil rendering yang membingungkan dapat disederhanakan menjadi salah satu dari lima aturan tersebut. Jika Anda ingin melihat bagaimana operator grafis ini berada di dalam model objek yang lebih besar, kamus halaman dan tabel cross-reference yang menunjuk ke mereka, tinjauan teknis struktur file PDF mencakup lapisan tersebut, dan membangun PDF sederhana dari awal menelusuri byte dari ujung ke ujung. Penggambaran teks hidup dalam keluarga operatornya sendiri dan memiliki jebakannya sendiri, dibahas dalam tulisan pendamping tentang penanganan teks dan font PDF

Pemanggilan gambar Delphi yang ditampilkan di sini, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage, dan ShowImage, adalah bagian dari HotPDF Component untuk Delphi dan C++Builder, yang memancarkan operator content stream ini untuk Anda