หน้า PDF ไม่ได้จัดเก็บพิกเซล และไม่ได้จัดเก็บโครงสร้างต้นไม้ของออบเจ็กต์รูปทรงอย่างที่ SVG ทำ มันจัดเก็บโปรแกรม ทุกเส้น, ส่วนโค้ง, การเติมสี, และรูปภาพที่วางบนหน้า ล้วนเป็นผลมาจากการรันลำดับตัวดำเนินการ (operators) ในสตรีมเนื้อหา (content stream) จากบนลงล่าง เทียบกับสถานะกราฟิกที่กำลังทำงานอยู่ เมื่อเข้าใจความจริงข้อนี้ข้อเดียว พฤติกรรมส่วนใหญ่ของรูปแบบไฟล์นี้ก็จะหมดความน่าประหลาดใจ: ทำไมการเติมสีถึงต้องใช้ตัวดำเนินการลงสีแยกต่างหากหลังจากสร้างเส้นทาง (path) เสร็จแล้ว, ทำไมสีและความกว้างของเส้นถึงรั่วไหลจากรูปทรงหนึ่งไปยังอีกรูปทรงหนึ่งเว้นแต่คุณจะครอบมันไว้, ทำไมโค้ดวาดภาพเดียวกันถึงไปลงเอยในที่ที่แตกต่างกันอย่างสิ้นเชิงได้หลังจากทำ coordinate transform เพียงครั้งเดียว นี่คือการทัวร์ชมโมเดลการทำงานนั้นตามที่กำหนดไว้ใน ISO 32000: ตัวดำเนินการที่คุณจะได้พบเมื่อเปิดสตรีมเนื้อหา และกฎที่ตัดสินว่าหมึกจะปรากฏที่ใดบนหน้า
สตรีมเนื้อหาคือ postfix bytecode
สตรีมเนื้อหาคือลำดับไบต์แบบแบนของตัวถูกดำเนินการ (operands) ตามด้วยตัวดำเนินการ (operators) ตัวถูกดำเนินการมาก่อน ส่วนตัวดำเนินการที่ใช้มันจะมาทีหลัง ซึ่งตรงข้ามกับการเรียกใช้ฟังก์ชัน และเหมือนกับสแต็กแมชชีน (stack machine): พุชตัวเลข จากนั้นออกคำสั่งกริยา โครงร่างสามเหลี่ยมคือโค้ดห้าบรรทัดนี้:
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
ตัวดำเนินการเหล่านี้ตั้งใจให้สั้นกระชับ หน้าของจริงจะมีตัวดำเนินการเหล่านี้หลายพันตัว และมักจะถูกบีบอัดด้วย FlateDecode ต้นทุนของความกะทัดรัดนั้นคือ สตรีมไม่มีโครงสร้างที่คุณสามารถคิวรีได้: โปรแกรมดูไม่สามารถถามได้ว่า "หัวข้อของหน้านี้อยู่ที่ไหน" มันทำได้แค่รันโปรแกรมและดูว่าหมึกตกลงไปที่ไหน นั่นคือเหตุผลพื้นฐานที่ว่าทำไมการแยกข้อความออกจาก PDF ใดๆ จึงเป็นเรื่องยาก
จุดกำเนิดอยู่ที่มุมล่างซ้าย และ Y จะเพิ่มขึ้นในทิศทางขึ้น
ก่อนที่พิกัดใดๆ จะสมเหตุสมผล คุณต้องรู้ก่อนว่า (0, 0) อยู่ที่ไหน PDF กำหนดจุดกำเนิดไว้ที่มุมล่างซ้ายของหน้า โดยค่า X จะเพิ่มขึ้นไปทางขวา และ Y จะเพิ่มขึ้นในทิศทางขึ้น โดยมีหน่วยวัดเป็นพอยต์ (points) ที่ 72 พอยต์ต่อนิ้ว (ISO 32000-2 §8.3.2) ในหน้าขนาด US Letter ขอบด้านบนจะอยู่ที่ y = 792 ไม่ใช่ y = 0 ใครก็ตามที่มาจากงานกราฟิกบนหน้าจอ ซึ่งจุดกำเนิดอยู่มุมบนซ้ายและ Y เพิ่มขึ้นในทิศทางลง จะสับสนเรื่องนี้ในครั้งแรกและวาดเส้นแรกหลุดขอบล่างของหน้าไป หน่วยวัดนี้ไม่ขึ้นอยู่กับสื่อที่ใช้ (medium): 72 หน่วยคือหนึ่งนิ้วเสมอ ไม่ว่าหน้าจะถูกเรนเดอร์บนหน้าจอโทรศัพท์หรือเครื่อง imagesetter
ไลบรารีการวาดหน้าส่วนใหญ่สืบทอดธรรมเนียมนี้มาโดยตรง ตัวอย่างเช่น ใน HotPDF การเรียก TextOut และ path จะวัดจากมุมล่างซ้ายเป็นพอยต์ทั้งหมด ดังนั้นค่าที่ใกล้เคียงกับความสูงของหน้าจะทำให้เนื้อหาไปอยู่ด้านบน:
// 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
ลำดับการเรียกนั้นจะถูกแปลงเป็นตัวดำเนินการ m, l, และ S เหมือนข้างบนพอดี ไลบรารีเป็นเพียงคนพิมพ์ดีดให้กับสตรีมเนื้อหา ไม่มีอะไรมากไปกว่านั้น และการรู้ว่ามันปล่อยตัวดำเนินการอะไรออกมา คือสิ่งที่จะช่วยให้คุณสามารถหาเหตุผลเกี่ยวกับผลลัพธ์ได้ เมื่อรูปทรงตกลงไปในที่ที่คุณไม่ได้คาดหวัง
สร้างเส้นทาง (path) แล้วจึงค่อยลงสี
PDF แยกการสร้างเส้นทาง (path construction) ออกจากการลงสีเส้นทาง (path painting) และการแยกนี้ไม่ใช่เรื่องจู้จี้จุกจิก ก่อนอื่นคุณต้องอธิบายรูปทรงด้วยตัวดำเนินการสร้าง ซึ่งจะไม่เพิ่มสิ่งใดที่มองเห็นได้ จากนั้นจึงออกตัวดำเนินการลงสีเพียงตัวเดียว เพื่อตัดสินใจว่าจะทำอย่างไรกับเส้นทางที่สะสมมา สามเหลี่ยมรูปเดียวกันสามารถเป็นโครงร่าง, เป็นการเติมสีทึบ, หรือเป็นทั้งสองอย่าง ขึ้นอยู่กับคำกริยาตัวเดียวที่คุณปิดท้าย
ตัวดำเนินการสร้างมีเพียงไม่กี่ตัว m เริ่มต้น subpath ใหม่ที่จุดหนึ่ง l เพิ่มส่วนของเส้นตรง (segment) c เพิ่มเส้นโค้ง cubic Bezier จากตัวถูกดำเนินการหกตัว ได้แก่ จุดควบคุมสองจุดและจุดปลายหนึ่งจุด re เป็นทางลัดที่เพิ่มสี่เหลี่ยมผืนผ้าทั้งรูปจากกลุ่มตัวเลข x, y, width, height (ความกว้าง, ความสูง) สี่ตัว h ปิด subpath ปัจจุบันกลับไปยังจุดเริ่มต้น ตัวดำเนินการเหล่านี้ไม่มีการวางหมึกลงบนหน้า มันแค่สะสมรูปทรงเรขาคณิตเท่านั้น
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
ตัวอย่างต้นฉบับใช้ตัวดำเนินการเส้นโค้ง y ซึ่งตอนนี้ล้าสมัยไปแล้ว ตัวดำเนินการ c พร้อมกับจุดสามจุดที่ชัดเจน คือรูปแบบที่คุณจะเห็นในทางปฏิบัติ และเป็นตัวที่คุณควรเลือกใช้ เมื่อมีเส้นทางแล้ว ตัวดำเนินการลงสีตัวเดียวจะทำงานให้เสร็จ คำศัพท์เหล่านี้มีน้อยและคุ้มค่าที่จะจำให้ขึ้นใจ เพราะทุกรูปทรงในทุกหน้าจะจบลงด้วยตัวใดตัวหนึ่งต่อไปนี้:
Sวาดเส้นขอบเส้นทาง (stroke) โดยใช้ความกว้างของเส้นและสี stroke ปัจจุบันfเติมพื้นที่ด้านใน (fill) โดยใช้สี fill ปัจจุบันและกฎ nonzero windingf*เติมพื้นที่โดยใช้กฎ even-odd ซึ่งมีความสำคัญกับรูปทรงที่ตัดกันเองและรูปทรงที่มีรูBเติมพื้นที่และวาดเส้นขอบในการดำเนินการเดียว;bจะปิดเส้นทางก่อนnไม่ลงสีอะไรเลย ซึ่งเป็นวิธีที่เส้นทางกลายเป็นขอบเขตการตัด (clip region) โดยไม่ทิ้งร่องรอยที่มองเห็นได้
กฎการวนรอบ (winding rule) คือส่วนที่ผู้คนมักเข้าใจผิด กฎ Nonzero (f, B) นับการข้ามที่มีเครื่องหมายบวกลบของรังสีจากจุดที่ทดสอบ และเติมพื้นที่ทุกที่ที่จำนวนนับไม่เป็นศูนย์ ดังนั้นรูจะยังคงว่างเปล่าก็ต่อเมื่อ subpath ของมันวนไปในทิศทางตรงกันข้ามกับอันนอก กฎ Even-odd (f*, B*) จะสลับทุกการข้ามโดยไม่สนทิศทาง หากรูปทรง "โดนัท" ออกมาเป็นสีทึบ แสดงว่าวงกลมด้านในถูกวนไปในทิศทางเดียวกันกับวงนอก และคุณต้องกลับด้านมันหรือสลับไปใช้กฎ even-odd
สีคือโหมด ไม่ใช่พารามิเตอร์
สีในสตรีมเนื้อหามีความเหนียวแน่น (sticky) เมื่อคุณตั้งค่าสี มันจะคงอยู่อย่างนั้นจนกว่าคุณจะตั้งค่าสีอื่น หรือคืนค่าสถานะก่อนหน้า ซึ่งเป็นเหตุผลว่าทำไมการเปลี่ยนสีที่ไม่มีการครอบ (bracketed) จะแอบย้อมสีทุกสิ่งที่วาดหลังจากนั้นอย่างเงียบๆ PDF ยังแยกสี fill และสี stroke เป็นการตั้งค่าสองอย่างที่เป็นอิสระต่อกัน โดยใช้ตัวดำเนินการตัวพิมพ์เล็กสำหรับ fill และตัวพิมพ์ใหญ่สำหรับ stroke แต่ละสเปซสีของอุปกรณ์ (device color spaces) มีรูปแบบย่อของตัวเอง:
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 เหมาะสำหรับเอาต์พุตบนหน้าจอ, DeviceCMYK คือสิ่งที่โปรดักชันงานพิมพ์คาดหวัง, และ DeviceGray เป็นตัวเลือกที่เล็กที่สุดสำหรับเนื้อหาขาวดำ device spaces สะดวกแต่ไม่มีการปรับเทียบ (uncalibrated): ค่า RGB แบบเดียวกันสามารถเรนเดอร์ออกมาแตกต่างกันบนหน้าจอสองหน้าจอได้ ซึ่งนี่คือปัญหาที่สเปซสีแบบอิง ICC (ICC-based color spaces) และเอาต์พุตอินเทนต์ PDF/A (PDF/A output intents) ถูกสร้างขึ้นมาเพื่อแก้ปัญหานี้ สำหรับงานที่สีเป็นเรื่องสำคัญยิ่ง คุณควรเลือกสเปซสีที่ปรับเทียบแล้วด้วย cs และ CS และตั้งค่าองค์ประกอบต่างๆ ด้วย sc และ scn แต่สำหรับเอกสารทั่วไป device shorthands ก็รับภาระส่วนนี้ไปได้ ไลบรารีจะห่อหุ้มสิ่งเหล่านี้ด้วย typed calls ตัวอย่างเช่น HotPDF จะรับค่า TColor เดียว และปล่อยตัวดำเนินการที่ตรงกันออกมา:
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;
สถานะกราฟิก และสแต็ก q/Q
ทุกสิ่งที่ไม่ใช่ตัวเส้นทางเองจะอยู่ในกราฟิกสเตต (graphics state) ได้แก่ current transformation matrix (เมทริกซ์การแปลงปัจจุบัน), สี fill และ stroke, ความกว้างของเส้น, รูปแบบเส้นประ, ขอบเขตการตัด (clip region), ค่าอัลฟ่า สถานะนี้เป็นโกลบอล (global) และเปลี่ยนแปลงได้ (mutable) ดังนั้นวิธีเดียวที่ปลอดภัยในการทำการเปลี่ยนแปลงแบบโลคอล คือการบันทึกสถานะทั้งหมด, ปรับแต่งมัน, วาด, แล้วดึงค่ากลับมา นั่นคือสิ่งที่ q และ Q ทำ q พุชสำเนาของสถานะปัจจุบันลงในสแต็ก; Q ดึงมันออกมา (pop) ทิ้งการเปลี่ยนแปลงทุกอย่างที่ทำตั้งแต่มี q ที่คู่กัน
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 และ Q ที่ไม่สมดุลกันเป็นสาเหตุทั่วไปที่ทำให้สตรีมเนื้อหาที่สร้างด้วยมือหรือที่ถูกนำมาต่อกันทำงานผิดพลาด q ที่หลงเหลือมาโดยไม่มี Q คู่กัน ทำให้สแต็กลึกเมื่อหน้าจบลง; Q ที่เกินมาก็ทำให้มัน underflow ไม่ว่าทางใดก็ตาม โปรแกรมดูอาจเก็บ clip หรือการแปลงรูปเก่าเอาไว้ให้มีผลต่อไป ทำให้เนื้อหาหายไปหรือไปลงเอยผิดที่ เมื่อกราฟิกหายไปโดยไม่มีเหตุผลที่เส้นทางจะอธิบายได้ ให้ตรวจสอบ state stack ก่อนเลย
CTM แปลงพิกัดทุกพิกัด
เมทริกซ์การแปลงปัจจุบัน (current transformation matrix - CTM) อยู่ระหว่างตัวเลขในตัวดำเนินการของคุณกับหน้ากระดาษจริงๆ พิกัดทุกจุดจะถูกคูณด้วย CTM ก่อนที่จะวาดอะไรลงไป ดังนั้นการเปลี่ยนเมทริกซ์จึงเป็นการเปลี่ยนตำแหน่งและลักษณะของภาพวาดที่ตามมาทั้งหมด โดยไม่ต้องแตะต้องพิกัดของเส้นทางแม้แต่พิกัดเดียว ตัวดำเนินการ cm นำเมทริกซ์ใหม่มาต่อท้าย (concatenate) เมทริกซ์ปัจจุบัน โดยรับตัวถูกดำเนินการหกตัวที่เชื่อมโยงกับเมทริกซ์แบบ affine (affine matrix) [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)
มีสองสิ่งที่ทำให้คนมักพลาด สิ่งแรก cm ใช้วิธีนำมาผสมรวมกัน (compose) มากกว่าการแทนที่ ดังนั้นการแปลงร่างต่างๆ จึงสะสมและลำดับมีความสำคัญ: การย่อขยายแล้วค่อยเลื่อนตำแหน่ง ไม่เหมือนกับการเลื่อนตำแหน่งแล้วค่อยย่อขยาย อย่างที่สอง การหมุนและการย่อขยายจะหมุนรอบจุดกำเนิดปัจจุบัน ไม่ใช่จุดศูนย์กลางรูปทรงของคุณ ดังนั้นเพื่อหมุนวัตถุอยู่กับที่ คุณต้องเลื่อนมันไปยังจุดกำเนิด, ทำการหมุน, แล้วเลื่อนกลับมา ทั้งหมดนี้ห่อหุ้มอยู่ภายใน q/Q เมทริกซ์เดียวกันนี้คือสิ่งที่ใช้จัดวางรูปภาพ ซึ่งเป็นส่วนสุดท้ายที่ควรดู
รูปภาพและเนื้อหาที่ใช้ซ้ำได้คือ XObjects
รูปภาพราสเตอร์ (Raster images) ไม่ได้อยู่ในบรรทัด (inline) ในสตรีมเนื้อหา มันถูกจัดเก็บเป็นรูปภาพ XObjects ซึ่งเป็นออบเจ็กต์ภายนอกที่มีดิกชันนารีของตัวเอง เพื่ออธิบายความกว้าง, ความสูง, bit depth, สเปซสี, และตัวกรองการบีบอัด ส่วนสตรีมเนื้อหาก็แค่สร้างจุดอ้างอิงถึงมัน ภาพถ่ายที่มีแบ็กกราวด์เป็น JPEG จะประกาศตัวมันเองในลักษณะนี้:
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % the image data is a JPEG stream
>>
รูปภาพ XObject จะวาดลงในยูนิตสแควร์ (unit square): มันจะใช้พื้นที่จากจุด (0, 0) ถึง (1, 1) เสมอในปริภูมิของผู้ใช้ (user space) คุณไม่ต้องส่งตำแหน่งหรือขนาดให้มัน แต่คุณตั้งค่า CTM เพื่อให้ unit square แมปไปเป็นกรอบสี่เหลี่ยมที่คุณต้องการ แล้วจากนั้นค่อยเรียกใช้ด้วย Do นั่นคือเหตุผลที่ว่าทำไมการวางรูปภาพจึงมักจะเป็นการแปลงพิกัด แล้วตามด้วยการเรียกใช้ (invocation) เสมอ โดยห่อหุ้มอยู่ในการบันทึก/ดึงกลับ (save/restore) เพื่อไม่ให้สเกลหรือการปรับขนาดไหลไปสู่ปฏิบัติการต่อไป:
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
กลไก Do แบบเดียวกันนี้ก็ใช้ในการขับเคลื่อน form XObjects ด้วย ซึ่งทำหน้าที่เก็บกลุ่มกราฟิกที่ใช้ซ้ำได้ ไม่ว่าจะเป็นโลโก้หรือตรายางที่ปั๊มซ้ำๆ ในฐานะสตรีมเนื้อหาของตัวเองที่มีขอบเขต (bounding box) นิยามมันเพียงครั้งเดียว และเรียกใช้ได้หลายครั้งด้วย CTM ที่ต่างกัน และไบต์ข้อมูลก็จะปรากฏในไฟล์แค่ครั้งเดียว ไลบรารีส่วนใหญ่ซ่อนกลไกนี้ไว้เบื้องหลังการเรียกวางแบบธรรมดา: HotPDF ลงทะเบียนภาพบิตแมป (bitmap) ด้วย AddImage และวางภาพนั้นด้วย ShowImage โดยรับค่าพิกัด x, y, ความกว้าง, และความสูง อย่างชัดเจน แทนที่จะขอให้คุณสร้างเมทริกซ์ขึ้นมาเอง:
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;
ภายใต้บรรทัดเดียวนั้น ไลบรารีจะเขียนดิกชันนารี XObject ของภาพ, ตั้งค่า CTM เพื่อกำหนดขนาดและตำแหน่งให้กับ unit square, แล้วปล่อย Do ออกมา โมเดลเบื้องหลังคือสิ่งที่ควรค่าแก่การเรียนรู้ เพราะมันอธิบายผลลัพธ์แปลกๆ ได้ทุกอย่าง: รูปภาพที่ยืดยาวคือ CTM ที่มีสเกลแฟกเตอร์ (scale factors) ไม่ตรงกัน, โลโก้ที่หน้าตาเหมือนกันเป๊ะบนหน้าสี่สิบหน้าคือ form XObject ตัวเดียวที่ถูกเรียกใช้สี่สิบครั้ง, และรูปภาพที่เรนเดอร์กลับหัวคือการพลิกเครื่องหมายในเมทริกซ์ ไม่ใช่ไฟล์เสีย
สิ่งนี้นำไปสู่อะไร
โมเดลกราฟิกนั้นดูเล็กนิดเดียวเมื่อคุณเห็นโครงร่างของมัน สตรีมเนื้อหาคือ postfix bytecode ที่ทำงานโต้ตอบกับสถานะที่เปลี่ยนแปลงได้ (mutable state); พิกัดเริ่มต้นที่มุมล่างซ้ายและผ่านค่า CTM; เส้นทางจะถูกสร้างอย่างเงียบๆ และถูกลงสีด้วยตัวดำเนินการที่จงใจสั่งการเพียงตัวเดียว; การตั้งค่าสีและเส้นจะคงอยู่ต่อไปจนกว่าคุณจะครอบมันด้วย q/Q; ภาพถ่ายและกราฟิกที่ใช้ซ้ำได้คือ XObjects ที่วางด้วยการแปลงพิกัดของ unit square ผลการเรนเดอร์ที่สับสนเกือบทุกอย่าง มักจะถูกทำให้เหลือแค่หนึ่งในห้ากฎเหล่านี้ หากคุณต้องการดูว่าตัวดำเนินการกราฟิกเหล่านี้นั่งอยู่ส่วนไหนในโครงสร้างโมเดลออบเจ็กต์ที่ใหญ่กว่า ดิกชันนารีหน้าและตารางการอ้างอิงไขว้ (cross-reference table) ที่ชี้ไปยังพวกมัน โครงสร้างไฟล์ PDF เป็นอย่างไร ครอบคลุมถึงเลเยอร์นั้น และการสร้างเอกสาร PDF ธรรมดาจากศูนย์ ก็จะพาเดินไปตามไบต์ตั้งแต่ต้นจนจบ การวาดข้อความอยู่ในกลุ่มตัวดำเนินการแยกต่างหากและมีหลุมพรางเป็นของตัวเอง ซึ่งครอบคลุมไว้ในบทความเสริม วิธีที่ PDF จัดการกับข้อความและฟอนต์
การเรียกการวาดภาพบน Delphi ที่แสดงที่นี่ MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage, และ ShowImage เป็นส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder ซึ่งปล่อยตัวดำเนินการในสตรีมเนื้อหาเหล่านี้ให้คุณ