Bài viết kỹ thuật

Nhập vector EMF trên Free Pascal với PDFlibPas

PDFlibPas chuyển đổi enhanced metafile thành nội dung trang PDF thực sự theo từng bản ghi thay vì raster hóa chúng, và đó là điều giữ cho một biểu đồ hay bản vẽ CAD nhập vào sắc nét ở mọi mức thu phóng. Bộ chuyển đổi đó dài khoảng 6500 dòng và được viết dựa trên VCL, nên khi thư viện có thêm mục tiêu Free Pascal, nó bị xếp loại là không thể chuyển nền và bị thay bằng stub. Cách xếp loại đó là sai, và cái cách nó sai lại là một bài học hữu ích về cách kiểm toán một phụ thuộc trước khi quyết định viết lại quanh nó

Bề mặt VCL thực sự của 6500 dòng đó hóa ra rất nhỏ: một lớp bitmap được dùng cho định dạng pixel, lưu luồng, handle, canvas và scanline; một lớp metafile được dùng cho chiều rộng, chiều cao và handle; cùng kiểu màu với hai hằng số. Mỗi thứ trong số đó đều đã được unit đồ họa của chính thư viện cung cấp sẵn, unit tồn tại chính là để bản build không-VCL có đối tượng tương đương. Bộ chuyển đổi chưa bao giờ bị chặn bởi VCL. Nó bị chặn bởi unit Windows của Free Pascal

Chia theo trục mà mã lệnh thực sự phụ thuộc

Vậy nên thay đổi không phải là một sự hiện thực lại. Nó là một conditional duy nhất: từ "biên dịch stub khi build không có VCL" thành "biên dịch stub khi không build cho Windows". Đó là trục đúng, và nói rõ lý do khiến sự khác biệt trở nên hiển nhiên. Enhanced metafile là một container của Windows. Bộ chuyển đổi là một trình phân tích bản ghi GDI của Windows từ đầu đến cuối. Việc ứng dụng chủ dùng VCL, một bộ widget khác, hay chẳng dùng bộ widget nào không liên quan gì đến việc những bản ghi đó có thể được giải nghĩa hay không; còn việc mục tiêu có phải là Windows hay không thì liên quan đến tất cả

Hậu quả của việc chọn đúng trục xuất hiện mà chẳng tốn công. Các bản build C++Builder — vốn hủy định nghĩa symbol nền tảng Windows trong thư viện này — giữ nguyên stub ném ngoại lệ và hành xử chính xác như trước. macOS giữ stub, một cách đúng đắn, vì ở đó không có bản ghi GDI nào để phân tích. Các bản build Delphi VCL không bị động đến. Và bản build Windows với bộ widget không phải VCL được nhập vector EMF như một tác dụng phụ mà không ai phải hiện thực. Một conditional thẳng với phụ thuộc thực sự biến công việc nền tảng thành thay đổi một dòng; một conditional lệch trục biến nó thành một cuộc viết lại mà chẳng bao giờ được lên lịch

Conditional nhập EMF được chuyển trục từ tư cách VCL sang nền tảng Windows, giữ stub ở nơi khác và cho các bản build Windows không-VCL có nhập vector
Chuyển trục điều kiện stub sang nền tảng Windows giữ nguyên hành vi mọi bản build hiện có và cho các mục tiêu Windows không-VCL nhập vector EMF miễn phí

Khoảng trống Free Pascal nằm ở khai báo, không phải logic

Thứ thực sự thiếu là các khai báo Win32 mà unit Windows của Delphi cung cấp còn của Free Pascal thì không. Gom chúng vào một unit tương thích duy nhất thay vì rải conditional khắp bộ chuyển đổi giữ cho parser dễ đọc. Danh sách này rất đáng suy ngẫm vì nó cho thấy mức phủ header chênh lệch thế nào giữa hai RTL: 113 hằng kiểu bản ghi metafile, hai cờ text-output mở rộng, ba hằng chế độ gradient fill, một kiểu con trỏ handle-table, các alias cho bản ghi đỉnh gradient và primitive, cùng ba kiểu bản ghi mà Free Pascal không khai báo chút nào, phủ alpha blending, blitting trong suốt và chế độ quản lý màu

Không mục nào trong số đó thú vị khi đứng riêng lẻ. Cả đống phải đúng trước khi parser biên dịch được, và một unit tương thích là ngôi nhà tự nhiên vì nó có thể được diff với tài liệu header như một khối

Các khai báo Win32 thiếu trong unit Windows của Free Pascal, được gom vào một unit tương thích cho bộ chuyển đổi vector EMF sang PDF
Hằng bản ghi, cờ, alias và ba kiểu bản ghi thiếu đều nằm trong một unit tương thích có thể diff với tài liệu header

Cái âm thầm vẽ ra bức tranh sai

Hai trong số các khai báo đó không chỉ là thiếu, chúng có mặt và sai cho mục đích này, và đây là phần đáng nhớ ngay cả khi bạn chưa từng đụng đến metafile

Free Pascal khai báo bản ghi tạo brush với cấu trúc brush thời gian chạy nhúng bên trong, và bản ghi bút mở rộng với cấu trúc bút thời gian chạy nhúng bên trong. Cả hai cấu trúc thời gian chạy đó khai báo thành viên hatch của chúng là một số nguyên cỡ con trỏ, vì trong một lệnh gọi GDI sống thì thành viên đó có thể mang một handle. Tuy nhiên metafile luôn lưu dạng 32-bit, vì bố cục bản ghi là một phần của định dạng tệp đã tuần tự hóa và không đổi theo độ rộng bit của tiến trình

Trên các bản build 32-bit, hai bên khớp nhau và chẳng có chuyện gì xảy ra. Trên Win64, thành viên cỡ con trỏ là tám byte trong khi tệp chỉ có bốn, nên mọi trường sau thành viên hatch đều bị đọc từ offset sai. Không có ngoại lệ, không có lỗi phân tích và không có cảnh báo. Metafile chỉ đơn giản là kết xuất sai: màu lấy từ những byte sai, độ rộng bút lấy từ những byte sai, và một bức tranh trông như bug kết xuất thay vì bug bố cục struct. Delphi phát hành các biến thể 32-bit tường minh của cả hai cấu trúc chính vì lý do này, và unit tương thích khai báo lại chúng theo cách đó

Bố cục byte của bản ghi brush EMF cho thấy trường hatch cỡ con trỏ đẩy các trường sau lệch bốn byte trên Win64 so với bố cục 32-bit cố định
Bản ghi tuần tự hóa luôn lưu hatch 4 byte, nên cấu trúc thời gian chạy cỡ con trỏ âm thầm đọc sai mọi trường phía sau trên Win64
// Sai trên Win64: Hatch cỡ con trỏ, tệp lưu 32 bit,
// và mọi trường phía sau lệch đi bốn byte mà không có lỗi nào
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 byte trong tiến trình 64-bit
  end;

// Đúng: bố cục tuần tự hóa, bề rộng cố định bất kể độ rộng bit
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // luôn 4 byte, đúng như lưu trong metafile
  end;

Quy tắc chung: bất kỳ cấu trúc nào vừa xuất hiện làm đối số API thời gian chạy vừa là bố cục trường tuần tự hóa đều cần hai khai báo, và bản tuần tự hóa phải dùng kiểu bề rộng cố định xuyên suốt. Thành viên cỡ con trỏ trong một định dạng tệp luôn là một bug đang chờ bản build 64-bit

Khác biệt chữ ký hàm nằm trong một wrapper, không phải tại từng call site

Những khác biệt còn lại là các lệch chữ ký hàm thông thường, và cách hấp thụ chúng là một wrapper chuyển tiếp thay vì một conditional tại từng call site. Hàm kết hợp biến đổi nhận con trỏ dưới Free Pascal trong khi Delphi nhận tham số tham chiếu, nên wrapper nhận tham chiếu và truyền địa chỉ. Nó cũng sao chép cả hai đối số nguồn vào biến cục bộ trước, vì bộ chuyển đổi có những call site nơi ma trận đích đồng thời là một trong các nguồn, và việc truyền cùng một địa chỉ hai lần cho một hàm vừa ghi vừa đọc tạo ra một biến đổi sai một cách tinh vi mà chỉ lộ ra trên nội dung bị xoay

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Sao chép trước: caller hợp lệ có thể truyền Dest làm A hoặc B
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

Các kiểu hình chữ nhật và điểm là trường hợp còn lại. Free Pascal coi các bản ghi hình chữ nhật và điểm của metafile là các kiểu khác biệt với các kiểu đồ họa chung, nên tám vị trí gán cần một phép ép kiểu tường minh giữa các bản ghi có bố cục giống hệt nhau. Cả hai trình biên dịch đều chấp nhận dạng ép kiểu, nên những vị trí đó không mang conditional nào, đáng đổi lấy chút xấu xí

Điều này thay đổi gì cho một bản triển khai Free Pascal

Nhập vector EMF chạy trên Windows dưới Free Pascal, tạo ra nội dung trang giống hệt bản build Delphi: path là path, gradient là nội dung pattern, văn bản là văn bản. Ngoài Windows, con đường raster vẫn là câu trả lời, và đó là giới hạn của định dạng chứ không phải của bản chuyển nền. Trạng thái tọa độ và clipping mà bộ chuyển đổi nạp vào được mô tả trong bài viết về bộ theo dõi CTM và clipping của content stream, còn các primitive vector nó phát ra được đề cập trong đồ họa vector, shader và gradient

Nếu bạn đang kiểm toán codebase của chính mình cho cơ hội tương tự, bài tập hữu ích chính là cái đã khởi nguồn cho chuyện này: liệt kê những thành viên bạn thực sự dùng từ framework mà bạn nghĩ mình phụ thuộc. Câu trả lời thường ngắn hơn nhiều so với những gì danh sách import gợi ý, và ràng buộc thực sự thường nằm hoàn toàn ở nơi khác. Các con đường nhập dựa trên device-context nói chung được mô tả trong bài viết về print preview và device context, và mức phủ nền tảng cùng chuỗi công cụ được liệt kê trên trang sản phẩm losLab PDF Developer Library