HotPDF, thành phần PDF VCL gốc cho Delphi và C++Builder, đánh giá ba loại hàm PDF được xây dựng từ công thức thay vì lưới mẫu: nội suy hàm mũ Type 2, ghép nối Type 3, và hàm tính toán PostScript Type 4, tương ứng với ISO 32000-1 §7.10.3, §7.10.4, và §7.10.5. Type 2 pha trộn giữa hai vector đầu ra dọc theo một đường cong, Type 3 nối chuỗi nhiều hàm con trên một miền đầu vào duy nhất, và Type 4 chạy một chương trình PostScript bị giới hạn có thể rẽ nhánh, so sánh, và tính toán gần như bất cứ điều gì một content stream cần từ đầu vào của nó. Chỉ cần sai một chút ở bất kỳ hàm nào trong ba hàm này thì lỗi sẽ không bao giờ tự lộ ra như một bug — nó hiện ra dưới dạng một gradient có một dải phẳng lì chết cứng, một spot color render ra màu đen tuyền, hoặc một hàm tính toán lệch đúng một đơn vị ở những đầu vào mà một bộ test tình cờ không thử qua
Ba loại này đứng cạnh loại thứ tư, Type 0, loại lưu một lưới đã lấy mẫu thay vì một công thức và được nói riêng trong bài viết đồng hành về bảng tra màu Type 0. Hai họ này giải quyết cùng một vấn đề, ánh xạ một đầu vào thành một đầu ra, nhưng Type 0 là dữ liệu được tính một lần và nướng sẵn vào file, còn Type 2, 3, và 4 là code mà trình đọc đánh giá ở mỗi lần gọi. Cả bốn loại chia sẻ một điểm phân phối duy nhất trong bộ render của HotPDF, được đánh khóa theo entry /FunctionType của từ điển hàm, nên một shading, một tint transform, hay một hàm halftone spot không bao giờ cần biết nó nhận được loại nào trong bốn loại trước khi có thể yêu cầu một màu
Một hàm mũ PDF Type 2 hoạt động như thế nào?
Một hàm PDF Type 2 tính một công thức duy nhất — y = C0 + x^N × (C1 − C0), áp dụng theo từng thành phần — trong đó x là đầu vào duy nhất của hàm, được chuẩn hóa theo /Domain của nó trước khi công thức chạy (ISO 32000-1 §7.10.3). /C0 và /C1 là các vector đầu ra ở hai đầu của khoảng đó, một số cho mỗi thành phần đầu ra, và /N là số mũ định hình đường cong giữa chúng: N = 1 cho ra dải tuyến tính thẳng đứng sau hầu hết các điểm dừng gradient và chuyển đổi duotone, N lớn hơn 1 kéo đường cong về phía C0, và N nằm giữa 0 và 1 đẩy nó về phía C1. RegisterExponentialFunction dựng từ điển đó từ năm tham số và trả về một đối tượng hàm sẵn sàng cắm vào một shading, một hàm halftone spot, hoặc bất cứ nơi nào khác mà spec chấp nhận một khóa /Function
Mối quan hệ về số lượng thành phần giữa C0 và C1 quan trọng theo hai cách: một lần khi bạn tạo một hàm Type 2, và một lần nữa bất cứ khi nào HotPDF phải render một hàm nó không tự tạo ra. Ở phía tạo dựng, RegisterExponentialFunction kiểm tra C0 và C1 với nhau và ném lỗi nếu chúng không khớp, nên một lệnh gọi đến được BeginDoc đã là một đối tượng hàm tự nhất quán. Nhưng ở phía render, bộ đánh giá phải tin tưởng vào bất cứ mảng /C0 và /C1 nào một file nguồn thực sự khai báo — chẳng hạn một file từ xưởng in được mở để xem trước, hay một tài liệu đã ký được hiển thị lại cho người dùng — và các phiên bản trước 2.376.0 đọc các mảng đó vào một bộ đệm có kích thước cho bốn thành phần, trường hợp CMYK. Một tint hàm mũ DeviceGray hoặc DeviceRGB, với /C0 và /C1 chỉ có một hoặc ba phần tử, thất bại âm thầm trong lần đọc đó và để cả hai mảng ở mức không, nên tint đó vẽ ra màu đen phẳng thay vì màu dự định của nó. Phiên bản 2.376.0 đã đổi kích thước bộ đọc theo đúng số lượng đầu ra khai báo thực tế của hàm thay vì một bộ đệm cố định — chính xác là kiểu lỗi mà chỉ một test case không phải CMYK mới phơi ra, vì bộ test hiện có chạy CMYK xuyên suốt, nơi bốn-vào-bốn luôn vừa khít
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;
Ghép nối Type 3: nối chuỗi hàm con qua một mảng Bounds
Một hàm PDF Type 3 ghép nối k hàm con thành một ánh xạ từng đoạn trên /Domain của một đầu vào duy nhất, và hai mảng làm điều đó hoạt động là /Bounds và /Encode (ISO 32000-1 §7.10.4). /Bounds chứa k − 1 điểm chia nội bộ cắt /Domain thành k khoảng liên tiếp; bộ đánh giá chọn khoảng đầu tiên có cận trên vượt quá đầu vào, hoặc khoảng cuối cùng một khi đầu vào chạm đến cận cuối, và chuyển giao cho hàm con của khoảng đó. /Encode sau đó ánh xạ lại đầu vào từ vị trí của nó bên trong khoảng đó sang bất cứ phạm vi đầu vào nào mà hàm con được chọn tự mong đợi — thường là [0, 1] nếu hàm con là thêm một đoạn hàm mũ nữa — trước khi việc đánh giá tiếp tục, sâu thêm một lệnh gọi, vào /Domain và /Range riêng của hàm con đó
Bộ đánh giá ghép nối của HotPDF trước đây chỉ xử lý đúng hai hàm con, và bộ đọc /Bounds của nó yêu cầu một mảng tám phần tử đầy đủ, nên điểm chia duy nhất mà một gradient hai đoạn thực sự cần — một số trong /Bounds — luôn phân tích thất bại và hàm trả về rỗng. /Encode hoàn toàn không được áp dụng. Phiên bản 2.376.0 đã viết lại việc lựa chọn thành phép tìm kiếm k-hàm-con tổng quát mà spec mô tả và bắt đầu đọc /Bounds theo đúng độ dài khai báo thực tế của nó, nên một gradient ba, bốn, hoặc năm điểm dừng được ghép từ ngần ấy đoạn hàm mũ giờ giải quyết đúng theo cách một gradient hai đoạn luôn tuyên bố sẽ làm. Ví dụ dưới đây dựng một dải hai đoạn từ đen qua đỏ đến trắng, hình dạng mà một shading axial hoặc radial tìm đến bất cứ khi nào một đường cong hàm mũ duy nhất không thể tải hết mọi điểm dừng màu mà một thiết kế đòi hỏi
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;
Một hàm tính toán PostScript Type 4 có thể làm được gì mà Type 2 và 3 không thể?
Một hàm PDF Type 4 chạy một chương trình đích thực, dù bị giới hạn có chủ đích: một máy tính PostScript đẩy các đầu vào của nó lên một stack toán hạng, thực thi các toán tử số học, so sánh, thao tác stack, và boolean cộng thêm các điều kiện if/ifelse, và để lại các đầu ra của nó trên stack khi hoàn tất (ISO 32000-1 §7.10.5, Bảng 42). Không có cấu trúc vòng lặp nào và không có lưu trữ biến có tên, chỉ có stack, điều này giữ cho một chương trình tuân thủ chuẩn dễ suy luận — nhưng trong tập toán tử bị giới hạn đó, Type 4 có thể diễn tả những điều Type 2 và Type 3 không thể, chẳng hạn một công thức pha trộn đa mực thật sự cho một separation DeviceN hay một hàm halftone spot với ngưỡng có điều kiện. Bộ đánh giá của HotPDF, HPDFEvalPostScriptCalculator, token hóa chương trình một lần — số, toán tử, và khối thủ tục { } — sau đó duyệt qua một stack toán hạng 100 mục, độ sâu mà ISO 32000-1 §7.10.5 yêu cầu, đằng sau một trần cứng 50.000 toán tử đã đánh giá như một tuyến phòng thủ chống lại các chương trình bệnh lý hoặc viết tay
Toán tử roll: hướng dễ bị làm ngược
roll là toán tử dễ bị làm ngược nhất trong lần thử đầu tiên, vì cả thứ tự tham số lẫn hướng xoay của nó đều chạy ngược với cách tiếng Anh mô tả chúng. n j roll lấy ra một số đếm n và một lượng xoay j, sau đó dịch chuyển tuần hoàn n mục trên cùng của stack theo j vị trí, cuộn các mục rơi khỏi một đầu quay lại đầu kia; ví dụ chuẩn, lấy thẳng từ spec, là a b c 3 1 roll cho ra c a b — mục trên cùng chuyển xuống đáy của nhóm, không phải theo chiều ngược lại, và mọi mục khác dịch lên một vị trí để nhường chỗ. Bộ đánh giá của HotPDF tính vị trí mới của mục stack i là (i + j) mod n, khớp chính xác với ví dụ đó, nhưng đây là một vòng lặp hai dòng cũng dễ viết sai với hướng xoay bị lật ngược, và một roll bị lật vẫn tạo ra một màu trông có vẻ hợp lý — chỉ là không phải màu mà tác giả file yêu cầu
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 không phải là Round của Delphi: làm tròn lên-nửa so với làm tròn kiểu ngân hàng
Toán tử round của PostScript luôn giải quyết một trường hợp hòa .5 về phía số nguyên lớn hơn, còn hàm Round tích hợp sẵn của Delphi thì không: nó làm tròn nửa-về-chẵn, quy ước làm tròn kiểu ngân hàng luân phiên chiều một trường hợp hòa .5 rơi vào để việc làm tròn lặp lại không tích lũy độ lệch. Hai cách này gần như khớp nhau ở mọi nơi và chỉ bất đồng đúng tại ranh giới quan trọng ở đây — Round(0.5) của Delphi trả về 0 và Round(2.5) trả về 2, trong khi round của spec PDF muốn 1 và 3 cho cùng những đầu vào đó — nên sự lệch pha này ẩn mình qua các phép test thông thường rồi tái hiện thành một lỗi lệch-một-đơn-vị nhất quán ở bất cứ đâu mà phép toán trung gian của một chương trình tính toán rơi đúng vào một số nửa-nguyên. ISO 32000-1 §7.10.5 Bảng 42 nói rõ ràng rằng round đẩy một phần thập phân .5 về phía số nguyên lớn hơn, nên HotPDF triển khai toán tử này thành Floor(x + 0.5) thay vì gọi Round của Delphi, và bất kỳ code nào triển khai lại hay kiểm tra thủ công phép toán của một chương trình Type 4 đều cần cùng phép thay thế đó
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;
Việc xác thực lúc đăng ký bắt lỗi một chương trình tính toán hỏng từ sớm
Một chương trình Type 4 lỗi định dạng thì rẻ để bắt được lúc tạo dựng và đắt để bắt ở bất cứ đâu khác, nên RegisterPostScriptFunction không chỉ lưu trữ mã nguồn: nó đánh giá thử chương trình một lần, tại điểm giữa của /Domain đã khai báo, trước khi đối tượng hàm được ghi vào tài liệu. Các khối { } không cân bằng, một toán tử không nhận diện được, một tình trạng cạn stack, hoặc số lượng đầu ra không khớp với /Range đều làm thất bại lượt chạy thử đó và ném ra ngoại lệ ngay lập tức, với call stack trỏ vào lệnh gọi RegisterPostScriptFunction thay vì vào một hiện tượng render bị phát hiện trong lúc QA trên một file đã phát hành. Lượt chạy thử tại điểm giữa không chứng minh chương trình đúng trên toàn bộ /Domain của nó — một nhánh điều kiện chỉ hoạt động sai gần một cạnh của phạm vi đầu vào vẫn có thể lọt qua một điểm mẫu duy nhất — nhưng nó loại bỏ toàn bộ nhóm chương trình bị hỏng về mặt cấu trúc thay vì chỉ sai ở một góc
Nơi gradient và spot color đưa các hàm này vào công việc
Type 2, 3, và 4 hiếm khi xuất hiện đơn lẻ trong một PDF thực tế; chúng xuất hiện ở bất cứ đâu spec chấp nhận một khóa /Function, và hai bên tiêu thụ phổ biến nhất là shading và tint transform của spot color. Toán tử sh của một gradient axial hay radial (ISO 32000-1 §8.7.4.5) đánh giá /Function của nó một lần cho mỗi vị trí dọc theo trục gradient, chính xác là trường hợp nhiều điểm dừng mà ghép nối Type 3 tồn tại để phục vụ. Tint transform của một không gian màu Separation hay DeviceN là nơi thường xuyên khác cho ba loại này, và đó là nơi Type 4 chứng tỏ giá trị của nó: một mực spot đơn thường quy giản về một đường cong Type 2 hoặc Type 0, nhưng một pha trộn DeviceN của nhiều mực với hành vi trapping và overprint thực sự thường cần logic điều kiện mà chỉ một máy tính PostScript mới diễn tả được, trường hợp được nói đến trong bài viết về render spot color Separation và DeviceN. RegisterSeparationFunc là lệnh gọi đi kèm ở phía tạo dựng: nó nhận một tên chất màu, một không gian màu thay thế, và bất kỳ đối tượng nào họ hàm Register*Function trả về, và nối tint transform đó vào một tài nguyên không gian màu Separation mà phần còn lại của trang có thể chọn bằng scn/SCN
Cùng nhau, các lưới đã lấy mẫu của Type 0 và ba loại dựa trên công thức này bao phủ mọi /Function mà một PDF có thể khai báo, và việc chọn đúng loại chủ yếu là câu hỏi về những gì bạn đã có sẵn: một bảng tra cứu tính ở nơi khác trở thành Type 0, một phép pha trộn hai đầu mút trở thành Type 2, nhiều phép pha trộn nối chuỗi qua một miền trở thành Type 3, và bất cứ thứ gì có logic điều kiện thực sự trở thành Type 4. RegisterExponentialFunction, RegisterStitchingFunction, và RegisterPostScriptFunction là một phần của HotPDF Component tiêu chuẩn dành cho Delphi và C++Builder, cùng với phần còn lại của API hàm và shading theo ISO 32000-1 của nó