HotPDF chuyển các package XPS và OpenXPS sang PDF bên trong Delphi và C++Builder mà không cần print driver, map mọi tọa độ fixed-page 96-DPI qua một ma trận trang scale 0.75 lật Y, phát hành mỗi VisualBrush thành một Form XObject dùng chung, và biến các chế độ tile ImageBrush thành các tiling pattern PDF gốc thay vì các lần vẽ ảnh lặp lại
Kịch bản kéo đa số các shop Windows vào chuyện này nhạt nhẽo và không thể tránh. Một thứ gì đó đã in ra Microsoft XPS Document Writer — một báo cáo ERP cũ, một biểu mẫu đã ký, một loạt sao kê — và chính sách lưu trữ nói PDF. XPS là một định dạng chụp bắt rất tốt và là một định dạng kinh khủng để trao cho hệ thống hồ sơ mười năm nữa. Vậy tệp spool phải trở thành một PDF từng trang một, và ngay khi bắt tay viết converter đó bạn phát hiện phần thú vị không nằm ở XML. Mà là XPS và PDF bất đồng về gốc tọa độ nằm ở đâu, một đơn vị đáng bao nhiêu, và một brush được phép là gì
Từ package sang PDF trong một lượt
Entry point là registry document handler, chứ không phải một lớp XPS đặc biệt. THPDFDocumentHandlerRegistry.RegisterStandardHandlers cài các handler XPS, EPUB và CBZ; việc nhận diện dựa trên nội dung, nên một package mang [Content_Types].xml cùng ít nhất một phần .fpage đạt 95 điểm kể cả khi phần mở rộng tệp nói dối, trong khi một phần mở rộng .xps hay .oxps trơn chỉ đạt 10 điểm. Thứ tự đó có ý nghĩa khi bạn nhận upload, vì kẻ tấn công đổi tên một EPUB thành .xps không được phép dẫn dắt pipeline
var
Handled: IHPDFHandledDocument;
Info: THPDFDocumentHandlerInfo;
Options: THPDFDocumentHandlerOptions;
Registry: THPDFDocumentHandlerRegistry;
Output: TFileStream;
begin
Registry := THPDFDocumentHandlerRegistry.Create;
Output := TFileStream.Create('spool.pdf', fmCreate);
try
Registry.RegisterStandardHandlers;
Options := THPDFDocumentHandlerOptions.Default;
if not Registry.OpenFromFile('spool.xps', '', Options, Handled, Info) then
raise Exception.Create('no registered handler recognised the package');
if Handled.Format = hdfXPS then
Handled.WritePDF(Output, Options, Info);
// Info.UnsupportedFeatureCount là số điểm trung thực cho lần chuyển đổi này
finally
Output.Free;
Registry.Free;
end;
end;
Mọi thứ về lần chuyển đổi đều được trù ngân sách trước khi thử. THPDFDocumentHandlerOptions.Default đặt trần entry lưu trữ ở 10,000, byte lưu trữ sau khi mở rộng ở 1 GiB, tỉ lệ nén ở 200, tài nguyên ở 4,096, và trang ở 10,000, đồng thời mang một CancellationToken tuỳ chọn để một job phía máy chủ có thể dừng giữa package. Sau đó đọc Info.UnsupportedFeatureCount và coi một giá trị khác không là một phát hiện thật: HotPDF cố tình đếm những gì nó không map được thay vì vẽ một xấp xỉ rồi im lặng về điều đó
Vì sao một trang XPS cần ma trận thay vì tọa độ viết lại?
Vì viết lại tọa độ làm mất stack transform. Một FixedPage XPS được đặc tả theo đơn vị 96-DPI với gốc ở trên trái và Y lớn dần xuống dưới; không gian người dùng PDF là 72-DPI với gốc ở dưới trái và Y lớn dần lên trên. Cách sửa ngây thơ là nhân mọi số với 0.75 và trừ từng Y khỏi chiều cao trang khi phát hành. Điều đó đúng cho một path phẳng và sụp đổ ngay khi một RenderTransform, một Canvas lồng nhau, hay một ma trận cấp brush xuất hiện, vì các transform đó được định nghĩa trong không gian XPS và phép viết lại theo từng tọa độ của bạn đã rời khỏi nó. HotPDF vì thế giữ phép chiếu thành một ma trận và kết hợp nó. HPDFXPSPageMatrix trả về các hằng số cố định một lần mỗi trang, HPDFMultiplyXPSMatrix nối nó với transform path tích luỹ, và kết quả được phát hành thành đúng một toán tử cm trước hình học. Dữ liệu path sau đó được viết bằng các số XPS không sửa đổi, và đó cũng là lý do cú pháp hình học viết tắt có thể dùng chung parser có biên giống như dữ liệu path SVG — chỉ token fill-rule F0 hay F1 ở đầu là do adapter XPS xử lý. Nếu bạn đã theo cùng lập luận đó cho nhập vector EMF và WMF, hình dáng lập luận sẽ quen thuộc: các định dạng nhập được chuyển đổi bằng ma trận, không bao giờ bằng số học trên tọa độ lá
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // đơn vị XPS 96-DPI sang điểm PDF 72-DPI
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // XPS Y lớn dần xuống, PDF Y lớn dần lên
Result.E := 0;
Result.F := PageHeight; // chiều cao trang PDF, theo điểm
end;
// Một CTM đã kết hợp cho mỗi visual, phát hành trước mọi toán tử path
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
Làm sao tái sử dụng một VisualBrush mà không vẽ nó hai lần?
Một VisualBrush vẽ một cây visual tuỳ ý — các con Canvas, Path và Glyphs — vào một vùng, có thể lặp khắp vùng đó. HotPDF biên dịch visual đó một lần thành một Form XObject PDF rồi đặt nó, cùng chiến lược tài nguyên được mô tả trong nhập SVG qua Form XObject. Hai chi tiết quyết định nó có chạy hay không. Thứ nhất, visual phải được đi qua như các con XML trực tiếp: một phép quét phẳng tìm các phần tử đáng-tile sẽ kéo các visual lồng nhau lên cấp trang và phá cả phạm vi tài nguyên lẫn thứ tự vẽ. Thứ hai, nội dung được chụp với ma trận trang XPS-sang-PDF đã áp dụng sẵn, nên việc phát hành Form đòi hỏi nhân với nghịch đảo của ma trận đó, nếu không mỗi lần đặt lại áp scale 0.75 và lật Y. Form cũng phải sở hữu tài nguyên của chính nó: HotPDF chỉ sao chép các font, XObject, pattern, ExtGState và không gian màu mà captured content stream thực sự tham chiếu; nhân bản toàn bộ dictionary tài nguyên trang sẽ kéo Form đang được đăng ký vào đồ thị tài nguyên của chính nó và dựng một vòng. Font nằm trong dictionary trực tiếp trên các trang thường và chỉ được thăng cấp thành dictionary indirect dùng chung khi captured content thực sự chứa một Tf, nên một tài liệu không có visual nào tái sử dụng được không phải trả tiền cho bộ máy đó. Lưu ý một ranh giới đặc tả đáng biết trước khi bạn nộp bug: ECMA-388 mục 13.4 yêu cầu cả ViewboxUnits lẫn ViewportUnits trên một VisualBrush phải là Absolute, nên đơn vị tương đối không phải một tính năng còn thiếu — chúng là đầu vào không đạt chuẩn, và HotPDF từ chối bịa ra ngữ nghĩa tọa độ cho chúng
Tiling ImageBrush: bốn chế độ, bốn kích thước ô
Các chế độ tile XPS map lên các tiling pattern PDF từ ISO 32000-1 mục 8.7.3 thay vì được bung thành các lần đặt ảnh lặp lại khắp vùng phủ, điều này giữ kích thước output và thời gian chuyển đổi độc lập với việc brush phủ bao nhiêu của trang. Phép map rất máy móc một khi bạn nhìn thấy nó: phản chiếu được biểu đạt bằng cách đặt các lần đặt đã lật gương bên trong một ô pattern và phóng ô cho khớp
Tile— một lần đặt, ô giữ nguyên viewport 1×1FlipX— hai lần đặt, ô nới rộng thành 2×1FlipY— hai lần đặt, ô cao thêm thành 1×2FlipXY— bốn lần đặt, ô phóng thành 2×2
Mỗi lần đặt mang hình chữ nhật clip riêng, vì một phép map Viewbox tràn khỏi ô con của nó sẽ loang sang phản chiếu bên cạnh. /Matrix của pattern là phần bắt mọi người. Một tiling pattern được neo vào không gian người dùng mặc định của captured content stream cha, chứ không vào trạng thái đồ hoạ hiện hành lúc pattern được chọn, nên ma trận phải kết hợp rõ ràng cả ba tầng — phép chiếu fixed-page, transform Path, và Transform cấp brush — thay vì dựa vào một CTM xung quanh. HotPDF cũng xác thực trước khi cấp phát: RegisterImageTilingPattern giới hạn một pattern ở 1,024 lần đặt và từ chối clip thoái hoá, ma trận không khả nghịch và chỉ số ảnh không hợp lệ. Nếu bạn muốn mô hình tổng quát phía PDF đằng sau điều này, tiling pattern và không gian màu Pattern trình bày các toán tử nền tảng
// Phép chiếu fixed-page gộp vào ma trận pattern, rồi đến phép biến đổi cấp brush
PatternMatrix := HPDFMatFromOps( 0.75 * PathMatrix.A, -0.75 * PathMatrix.B,
0.75 * PathMatrix.C, -0.75 * PathMatrix.D,
0.75 * PathMatrix.E,
PageHeight - 0.75 * PathMatrix.F);
if Brush.HasTransform then
PatternMatrix := HPDFMatMul(PatternMatrix, BrushMatrix);
PatternName := Document.RegisterImageTilingPattern(Resource.ImageIndex,
Brush.Viewport.Left, Brush.Viewport.Top,
Brush.Viewport.Left + CellWidth, Brush.Viewport.Top + CellHeight,
CellWidth, CellHeight, Placements, pttNoDistortion, PatternMatrix);
Chuyện gì xảy ra khi một radial gradient không phải hình tròn?
XPS định nghĩa một RadialGradientBrush với GradientOrigin, Center, RadiusX và RadiusY, nên brush là một elip. Shading PDF type 3, ở ISO 32000-1 mục 8.7.4.5.4, hoà trộn giữa hai hình tròn và không có cách nào biểu đạt một elip trực tiếp. Lấy trung bình hai bán kính thành một con số là lối tắt hấp dẫn và nó sai thấy rõ trên bất kỳ brush nào không gần tròn. HotPDF thay vào đó dời bài toán vào hệ tọa độ: nó scale Y theo RadiusY / RadiusX, đăng ký một shading tròn trung thực trong không gian đã scale đó, chọn pattern, và ngay lập tức phát hành phép scale nghịch đảo để hình học path viết tiếp theo vẫn nằm trong không gian người dùng XPS gốc
ScaleY := Brush.RadiusY / Brush.RadiusX;
PatternName := Document.RegisterMultiStopRadialGradient(
Brush.StartX, Brush.StartY / ScaleY, 0,
Brush.EndX, Brush.EndY / ScaleY, Brush.RadiusX,
StopPositions, StopColours, 3);
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(ScaleY) + ' 0 0 cm'#10);
Page.SetFillPattern(PatternName); // pattern chụp CTM ngay tại đây
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
Thứ tự trong đoạn code đó là toàn bộ mẹo, và nó không phải chuyện phong cách. Một shading pattern PDF chụp ma trận biến đổi hiện hành tại thời điểm nó được chọn làm màu hiện hành, nên phép scale tạm thời phải được phát hành trước SetFillPattern hay SetStrokePattern, và phép nghịch đảo phải đi sau lựa chọn nhưng trước các toán tử path. Sai thứ tự theo hướng nào thì bạn có một gradient render đúng trên path đầu tiên và lệch dần trên mọi path kế tiếp. Một ràng buộc liên quan áp dụng cho chế độ tọa độ tương đối: RadiusX và RadiusY phải được phân giải theo chiều rộng và chiều cao của path một cách riêng biệt, vì scale cả hai bằng một chiều dài cạnh duy nhất lặng lẽ đổi tỉ lệ khung hình của elip trên mọi path không vuông
Nơi chuyển đổi thành thật về giới hạn của nó
Một số cấu trúc XPS được chuyển đổi xấp xỉ và một số hoàn toàn không được chuyển đổi, và lựa chọn thiết kế xuyên suốt là đếm chúng thay vì làm giả chúng. Các phần TIFF và JPEG XR được rasterise qua WIC và không mang lời hứa nào về alpha được giữ lại, trong khi PNG có kênh alpha hợp lệ được tách thành một ảnh gốc cộng một /SMask. Kích thước nội tại của ảnh được suy ra theo pixel * 96 / DPI, đọc PNG pHYs hay mật độ JPEG JFIF trước và quay về 96 DPI nếu không có, nên một header mật độ hỏng đáp xuống một kích thước đoán được thay vì một kích thước tùy tiện. Tài nguyên ma trận chưa phân giải, transform tương đối không chuẩn, ColorConvertedBitmap, các chế độ lan toả gradient không được hỗ trợ và hình học dị dạng đều làm tăng UnsupportedFeatureCount, và đầu vào dị dạng fail closed thay vì thoái hoá thành một bản vẽ khác mà im lặng
Đó là tư thế hữu ích cho một converter lưu trữ: một lần chuyển đổi lặng lẽ xấp xỉ tệ hơn một lần cho bạn biết nó không biểu đạt được đúng bốn phần tử nào, vì chỉ cách thứ hai mới cho bạn thứ để soát trước khi tài liệu được niêm phong vào hệ thống hồ sơ. Nếu bạn đang đánh giá chuyển đổi XPS và OpenXPS cùng với phần còn lại của pipeline tài liệu — dựng trang, font, ký số, output PDF/A — trang HotPDF Delphi PDF component liệt kê toàn bộ tập tính năng và các phiên bản Delphi và C++Builder được hỗ trợ