Bài viết kỹ thuật

Nhập vector EMF và WMF vào PDF Delphi bằng HotPDF

HotPDF, thành phần PDF Delphi và C++Builder gốc, nhập các metafile EMF và WMF của Windows bằng cách thông dịch trực tiếp từng bản ghi GDI thành các toán tử PDF thay vì làm phẳng file thành bitmap: gradient fill trở thành pattern axial shading của PDF, hatch brush trở thành tiling pattern của PDF, và một cổng kiểm soát trạng thái path tập trung ngăn các bản ghi lỗi định dạng làm hỏng đầu ra. Bất kỳ biểu đồ nào mà TChart, một bề mặt GDI+, hay một TCanvas đơn giản có thể xuất ra dưới dạng enhanced metafile đều là ứng viên cho đường xử lý này, và sự khác biệt lộ ra ngay khi ai đó phóng to trang hoặc gửi nó đến máy in độ phân giải cao

Phương án thay thế mà hầu hết lập trình viên Delphi mặc định chọn là raster hóa metafile thành bitmap trước khi đặt lên trang, và cái giá phải trả chỉ lộ ra sau này: một biểu đồ cột từng sắc nét trên màn hình trở nên rõ rệt lởm chởm ngay khi PDF được in ở 600 DPI hoặc chiếu lên màn hình phòng họp, và một vùng CAD tô hatch sụp thành một hình chữ nhật xám phẳng duy nhất nếu kiểu tô không được giữ lại. Đọc metafile như một chương trình thay vì một bức ảnh là cách tránh cả hai vấn đề trên, và đó cũng là con đường khó triển khai đúng hơn, đó là lý do những cạm bẫy dưới đây đáng để biết trước khi một báo cáo được phát hành

Vì sao thông dịch metafile thay vì làm phẳng nó thành bitmap?

HotPDF giữ việc nhập EMF và WMF trên đường xử lý vector vì một metafile Windows là một chuỗi các lệnh gọi vẽ GDI đã được ghi lại, không phải một bức ảnh, và việc phát lại các lệnh gọi đó thành các toán tử path, text, và shading của PDF chính là điều giúp kết quả co giãn giống như phần còn lại của trang. THPDFPage.ShowMetafile và người anh em ShowMetafileEx của nó là các điểm vào mà ứng dụng gọi tới, và cả hai đều giao metafile cho THPDFWmf, lớp duyệt qua từng bản ghi GDI và dịch nó. Ranh giới này không tuyệt đối, và HotPDF không giả vờ khác đi: một bản ghi metafile thực sự là dữ liệu raster, chẳng hạn một lệnh blit bitmap StretchDIBits, được nhúng dưới dạng một Image XObject PDF thật thông qua AddImageShowImage, cùng cặp lệnh gọi mà bất kỳ hình ảnh nào khác trên trang cũng đi qua, thay vì bị ép thành các toán tử path vốn không thể diễn tả một bức ảnh chụp. Đường nét, vùng tô, và văn bản vẫn giữ dạng vector; các pixel vốn đã là pixel trong nguồn vẫn là pixel trong đầu ra. Lệnh gọi đơn giản nhất không cần gì ngoài metafile đã nạp:

var
  Pdf: THotPDF;
  Chart: TMetafile;
begin
  Pdf := THotPDF.Create(nil);
  Chart := TMetafile.Create;
  try
    Chart.LoadFromFile('quarterly-revenue.emf');  // exported from TChart or GDI+
    Pdf.FileName := 'quarterly-report.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafile(Chart);
    Pdf.EndDoc;
  finally
    Chart.Free;
    Pdf.Free;
  end;
end;

Trình thông dịch chuyển tọa độ GDI sang không gian trang PDF như thế nào?

HotPDF trả lời câu hỏi đó bằng một lượt duyệt duy nhất qua chính chuỗi bản ghi của metafile, thay vì triển khai lại GDI lần thứ hai. THPDFWmf.Analyse đọc header của metafile qua lệnh gọi Win32 GetEnhMetaFileHeader, đặt lại trạng thái vẽ nội bộ, và gọi EnumEnhMetafile, cùng API liệt kê mà một trình xem metafile sẽ dùng, nên mỗi bản ghi EMR_* đến được THPDFWmf.ExecuteRecord theo đúng thứ tự nó được ghi ban đầu. GDI biểu diễn tọa độ theo hướng từ trên xuống trong các đơn vị thiết bị hoặc đơn vị logic do chế độ mapping riêng của metafile chọn; một trang PDF thì theo hướng từ dưới lên, tính bằng điểm trong không gian người dùng, hệ tọa độ được nói đến trong mô hình vẽ canvas của HotPDF cho path và fill. Mỗi trình xử lý bản ghi giải quyết sự khác biệt đó thông qua ScaleXScaleY, hai hàm gọi ProjectXProjectY để phát lại chính công thức window-to-viewport của GDI cho các chế độ mapping anisotropic và isotropic, nên một hình được ghi với chiều rộng năm đơn vị logic sẽ có chiều rộng đúng tính bằng điểm PDF bất kể ứng dụng nguồn đã đặt phạm vi window và viewport nào

Một gradient fill của GDI trở thành shading pattern của PDF như thế nào?

Một bản ghi EMR_GRADIENTFILL trở thành một pattern axial shading Type 2 thật của PDF (ISO 32000-1 §8.7.4.5) bất cứ khi nào GDI ghi lại nó theo một trong hai chế độ hình chữ nhật. THPDFWmf.VEMRGradientFill đọc trực tiếp bố cục riêng của bản ghi từ vùng đệm byte thô, theo đúng cấu trúc MS-EMF §2.3.1.6: một mảng đỉnh gồm các góc RGBA 16-bit, theo sau là danh sách hình chữ nhật, mỗi hình tham chiếu đến hai trong số các đỉnh đó. Với GRADIENT_FILL_RECT_H, màu sắc quét từ trái sang phải dọc theo đường giữa nằm ngang của hình chữ nhật; với GRADIENT_FILL_RECT_V, chúng quét từ trên xuống dưới dọc theo đường giữa thẳng đứng. Dù theo cách nào, hai màu góc và tọa độ hình chữ nhật đã chiếu đều đi thẳng vào THotPDF.RegisterAxialGradient, hàm trả về tên pattern, và trang vẽ hình chữ nhật rồi tô nó thông qua pattern đó (SetFillPattern) thay vì một lệnh gọi SetRGBFillColor phẳng, nên một tiêu đề dạng dải màu kiểu bảng tính hay một vùng biểu đồ gradient vẫn giữ được sự pha trộn thay vì sụp về một màu trung bình duy nhất

Chế độ tam giác Gouraud là khoảng trống thành thật cần thừa nhận. Khi trường ulMode của bản ghi báo GRADIENT_FILL_TRIANGLE, VEMRGradientFill nhận diện điều đó, ghi log rằng chế độ tam giác chưa được triển khai, và bỏ qua hình chữ nhật thay vì đoán mò một xấp xỉ hai màu. Việc nội suy theo từng đỉnh, từng pixel trên một lưới tam giác tùy ý không thể quy giản về một shading axial hoặc radial hai điểm dừng, và để diễn tả nó đúng cách sẽ cần phát ra một mesh shading Type 4 hoặc Type 5 của PDF, cùng họ shading mà bộ render trang của HotPDF cũng để trống khi đọc lại một PDF. Hai đường code không liên quan lại gặp nhau ở cùng một ranh giới: mesh shading là khoảng trống ở cả phía ghi lẫn phía đọc, và một sơ đồ nguồn dùng tam giác Gouraud để tạo hiệu ứng phát sáng radial mượt sẽ rơi về bất cứ brush đặc nào được dùng cuối cùng, chứ không phải một xấp xỉ được render

Hatch brush trở thành tiling pattern, không bị làm phẳng thành xám

Một hatch brush của GDI giữ được kết cấu của nó trong PDF vì THPDFWmf.SetBrushColor kiểm tra CurrentBrush.lbStyle xem có phải BS_HATCHED hay không trước khi rơi về một fill đặc, và chuyển trường hợp đó sang SetHatchBrushPattern thay thế. Phương thức đó ghi ra một content stream PDF 8x8 đơn vị gồm các toán tử vẽ đường stroke, m, l, và S, được chọn theo kiểu hatch của GDI: một nét kẻ ngang hoặc dọc đơn cho HS_HORIZONTALHS_VERTICAL, ba đường chéo song song cho HS_FDIAGONALHS_BDIAGONAL, và tổ hợp ngang-cộng-dọc hoặc cả hai đường chéo cho HS_CROSSHS_DIAGCROSS. THotPDF.RegisterTilingPattern đăng ký content stream đó như một tiling pattern có màu (PaintType 1, ISO 32000-1 §8.7.3.1) với XStepYStep 8 đơn vị, và trang tô màu thông qua SetFillPattern theo cùng cách một axial shading làm. Một bản vẽ mặt bằng CAD hay một bản vẽ kỹ thuật dựa vào hatch fill để phân biệt vật liệu sẽ giữ được ngôn ngữ hình ảnh đó trong PDF thay vì mất mọi vùng về cùng một màu xám

Không phải brush nào cũng nhận được cách xử lý đó, và khoảng trống này đáng biết trước khi một bản nhập CAD được phát hành. EMR_CREATEDIBPATTERNBRUSHPT, bản ghi cho một pattern brush ảnh bitmap tùy chỉnh thay vì một trong sáu kiểu hatch chuẩn của GDI, chỉ đăng ký handle của nó để các bản ghi SELECTOBJECTDELETEOBJECT sau đó vẫn nhất quán; HotPDF chưa phơi bày một pipeline tài nguyên Pattern PDF cho ảnh tile tùy ý, nên việc chọn brush đó sẽ rơi về một fallback màu đặc thay vì kết cấu gốc. Nếu một vùng tô hiện ra phẳng lì trong khi bản gốc rõ ràng dùng một kết cấu ảnh lặp lại, brush nguồn gần như chắc chắn là một pattern DIB tùy chỉnh chứ không phải hatch chuẩn, và đó là trường hợp duy nhất đáng kiểm tra thủ công trước. Cấu hình việc nhập cho một bản vẽ như vậy vẫn đi qua cùng một đối tượng tùy chọn:

var
  Pdf: THotPDF;
  Drawing: TMetafile;
  Options: THPDFEmfOptions;
begin
  Pdf := THotPDF.Create(nil);
  Drawing := TMetafile.Create;
  Options := THPDFEmfOptions.Create;
  try
    Drawing.LoadFromFile('floor-plan.emf');
    Options.Assign(Pdf.EmfOptions);   // start from the document-wide defaults
    Options.Redraw := False;          // interpret the original EMF bytes, no GDI re-record pass
    Options.ShowNullBrush := True;    // keep explicitly unfilled CAD regions visible
    Options.UseFrame := True;         // clip output to the frame the EMF header declares
    Pdf.FileName := 'floor-plan.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
    Pdf.EndDoc;
  finally
    Options.Free;
    Drawing.Free;
    Pdf.Free;
  end;
end;

Điều gì ngăn một metafile lỗi định dạng làm hỏng trang?

Câu trả lời của HotPDF là một cổng kiểm soát duy nhất ở đầu ExecuteRecord, thay vì một kiểm tra phòng vệ lặp lại trong từng cái của khoảng tám mươi trình xử lý bản ghi. Một dấu ngoặc path của GDI, mở bằng EMR_BEGINPATH và đóng bằng EMR_ENDPATH hoặc EMR_ABORTPATH, được theo dõi bởi một thuộc tính riêng PathContinue dựa trên trường FPathContinue. Khi dấu ngoặc đó còn mở, ExecuteRecord chỉ cho qua các bản ghi xây dựng path, các biến thể move, line, polyline, polygon, polybezier, và polydraw, cộng thêm CLOSEFIGURE và một tập nhỏ các bản ghi biến đổi và trạng thái DC như SETWORLDTRANSFORM, SAVEDC, và RESTOREDC. Mọi loại bản ghi khác đến ExecuteRecord khi dấu ngoặc còn mở, chẳng hạn một EXTTEXTOUT lạc lõng hay một lệnh blit bitmap, đều bị loại bỏ tập trung bằng một lệnh Exit duy nhất ngay khi nó đến

Cổng đó tồn tại vì một dấu ngoặc path trong một metafile viết tay, do công cụ tạo ra, hoặc đơn giản là bị hỏng, không có gì đảm bảo chỉ chứa những gì một file đúng chuẩn sẽ đặt giữa bản ghi mở và đóng của nó. Một bản ghi xuất văn bản rơi vào giữa EMR_BEGINPATHEMR_ENDPATH, nếu không có cổng kiểm soát, sẽ hoặc làm nhiễm bẩn hình học path đang được xây dựng, hoặc phát ra một toán tử hiển thị văn bản PDF giữa một chuỗi lẽ ra chỉ thuần túy xây dựng path, và cả hai kiểu lỗi đều là loại chỉ lộ ra trên một đầu vào lỗi định dạng từ một công cụ bên thứ ba, chứ không phải thứ mà một bộ test thông thường tình cờ bao phủ. Tập trung kiểm tra vào ExecuteRecord nghĩa là từng trình xử lý VEMR* riêng lẻ không cần tự phòng vệ trước việc bị gọi sai thời điểm; cổng kiểm soát quyết định điều đó một lần, trước khi phân phối, thay vì tám mươi lần sau đó

Đặt một biểu đồ vector cạnh văn bản và hình ảnh trên cùng một trang

Một trang báo cáo hiếm khi chỉ chứa một biểu đồ, và ShowMetafile kết hợp với các toán tử trang khác của HotPDF y hệt như bất kỳ lệnh vẽ nào khác. Một tiêu đề vẽ bằng TextOut, một biểu đồ cột tô hatch được nhập dưới dạng EMF, và một logo đặt bằng ShowImage đều có thể nằm trên cùng một trang trong cùng một content stream, mỗi thứ vẫn giữ độ trung thực gốc của nó, mẫu bố cục kết hợp này được nói đến trong hướng dẫn của HotPDF về bố trí văn bản, font, và hình ảnh trong một báo cáo:

Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart);   // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);

Trình thông dịch EMF và WMF, các pattern axial shading nó đăng ký cho gradient fill, và ánh xạ tiling pattern cho hatch brush được mô tả ở đây đều đi kèm sẵn trong HotPDF Component tiêu chuẩn dành cho Delphi và C++Builder, một thư viện VCL gốc không phụ thuộc DLL ngoài cho bất kỳ phần nào trong số này