PDFlibPas, PDF library losLab untuk Delphi, mengubah record Poly* EMF menjadi path PDF dengan mengikuti definisi tiap record di [MS-EMF]: EMR_POLYBEZIER 32-bit mulai dari titik 0, polyline tetap terbuka dan hanya di-stroke, PT_CLOSEFIGURE di EMR_POLYDRAW adalah flag, dan setiap jumlah titik dicek terhadap ukuran record. Aturan-aturan itu mendarat melintasi v3.539.39, v3.539.41, dan v3.539.43. Sebelumnya, chart laporan bisa keluar dari ImportEMFFromFile dengan irisan terisi di tempat seharusnya garis tren, kurva Bezier yang menekuk ke control point yang salah, atau outline tertutup yang kehilangan sisi terakhirnya. Tak satu pun dari itu me-raise error, dan aturan-aturan ini berlaku untuk converter EMF ke PDF Delphi mana pun maupun parser record GDI
Kenapa record Poly* EMF salah saat konversi ke PDF?
Record Poly* EMF bisa salah karena masing-masing membawa sebagian maknanya di luar titik-titiknya: apakah figure-nya terbuka, apakah mulai dari current position, pen dan brush mana yang dipakai, dan di mana titik-titik mulai di dalam record. Enhanced metafile adalah rekaman panggilan GDI terhadap sebuah device context, jadi converter harus me-replay state device context itu, bukan cuma koordinatnya. PDF tak punya device context. PDF punya path, sebuah current point di dalam path itu, dan painting operator yang memutuskan antara stroke (S), fill (f), atau keduanya (B). Setiap ketidakcocokan antara kedua model itu menjadi perbedaan render yang senyap
Keluarga Poly* juga datang dalam dua lebar. Setiap record 32-bit seperti EMR_POLYLINE punya kembaran 16-bit seperti EMR_POLYLINE16 yang menyimpan titik sebagai pasangan SmallInt. GDI biasanya merekam bentuk compact-nya kalau semua koordinat muat, sehingga handler 32-bit milik converter bisa salah bertahun-tahun sementara gambar test sehari-hari tak pernah menyentuhnya. Audit tercepat adalah menjalankan titik yang sama lewat kedua record lalu membandingkan path hasilnya. Record-record yang dibahas di sini semuanya ada di grup drawing record milik [MS-EMF] (2.3.5 Drawing Record Types)
| Record | Mulai di | Tertutup? | Current position |
|---|---|---|---|
EMR_POLYBEZIER | Titik 0 | Tidak | Tak dipakai, tak diperbarui |
EMR_POLYLINE | Titik 0 | Tidak (hanya pen) | Tak dipakai, tak diperbarui |
EMR_POLYLINETO | Current position | Tidak (hanya pen) | Dipakai dan diperbarui |
EMR_POLYPOLYLINE | Titik pertama tiap polyline | Tidak (hanya pen) | Tak dipakai, tak diperbarui |
EMR_POLYDRAW | PT_MOVETO pertama, atau current position | Hanya di mana PT_CLOSEFIGURE diset | Dipakai dan diperbarui |
Sebenarnya di mana kurva EMR_POLYBEZIER mulai?
Kurva EMR_POLYBEZIER mulai dari titik 0, dan hanya titik dari indeks 1 ke atas yang dikelompokkan bertiga sebagai control point, control point, end point. Record dengan 7 titik karenanya menggambar dua segmen cubic: 0 adalah start, 1 sampai 3 membentuk segmen pertama, 4 sampai 6 membentuk segmen kedua. Handler 16-bit di PDFlibPas sudah melakukannya dengan benar. Handler 32-bit mulai mengelompokkan dari titik 0, jadi titik start termakan sebagai control point pertama dan setiap segmen berikutnya bergeser satu. Kurvanya tetap terrender, cuma kurva yang salah. Sejak v3.539.41 kedua lebar membuka path dengan m di titik 0 dan menerbitkan satu c per triple lengkap setelahnya
Untuk parser Anda sendiri: count yang bukan 1 plus kelipatan 3 berarti malformed, dan titik-titik ekornya sebaiknya diabaikan daripada dijahit ke dalam kurva
PolyDraw: PT_CLOSEFIGURE adalah flag, bukan tipe titik
Di EMR_POLYDRAW, PT_CLOSEFIGURE (nilai 1) adalah bit yang digabungkan dengan PT_LINETO (2) atau PT_BEZIERTO (4), jadi type byte yang valid bisa 3 atau 5. Tipe titik adalah byte itu dengan bit dimask-off, dan flag-nya berarti tutup figure setelah segmen yang berakhir di titik ini. Handler PDFlibPas yang lama mencocokkan byte terhadap nilai tunggal di case statement, sehingga titik bertipe 3 dan 5 tak cocok dengan apa pun dan dilewati sepenuhnya. Rectangle yang digambar dengan PolyDraw kehilangan sisi penutupnya, dan triple Bezier yang titik terakhirnya membawa flag kehilangan titik itu, yang membuat setiap triple setelahnya kehilangan irama
Sejak v3.539.39 tipenya dibaca sebagai Types[i] and not PT_CLOSEFIGURE, dan close-nya diterbitkan hanya setelah satu segmen penuh: setelah garis untuk PT_LINETO yang tertutup, dan setelah titik ketiga sebuah grup Bezier. File malformed yang menyetel flag di titik pertama atau kedua sebuah triple tak menutup figure lebih awal. Dua perbaikan terkait lain ikut keluar di release yang sama:
- Setiap
PT_MOVETOdiEMR_POLYDRAW1616-bit me-restart seluruh path, sehingga record yang memuat tiga figure hanya menyisakan yang terakhir; kini move pertama memulai path dan move selanjutnya membuka subpath - Record PolyDraw yang tak diawali
PT_MOVETOmulai dari current position, sesuai definisi record-nya, alih-alih menulis operatorlatauctanpampendahulu
Kenapa polyline EMF tak boleh pernah di-fill di PDF?
Polyline EMF tak boleh pernah di-fill karena EMR_POLYLINE dan EMR_POLYPOLYLINE adalah figure terbuka yang digambar dengan pen saja, dan meng-fill path terbuka di PDF diam-diam menutupnya. ISO 32000-1 §8.5.3 menyatakan bahwa fill operator menutup subpath terbuka mana pun sebelum mengecatnya. Converter yang menerbitkan B atau f untuk polyline tiga titik karenanya mengecat segitiga terisi dengan warna brush saat itu: irisan terisi di bawah garis tren chart. Sebelum v3.539.41, PDFlibPas meng-fill kedua lebar polyline dengan brush, dan record 32-bit-nya juga ditutup eksplisit. Kini kedua lebar berakhir dengan stroke saja, dan pembedaan GDI dijaga: Polygon menutup dan meng-fill, Polyline tak pernah
PolylineTo mulai dari current position
EMR_POLYLINETO menggambar dari current position melewati setiap titik di record, tetap terbuka, dan membiarkan current position di titik terakhir. Handler lama juga memuat special case yang mematikan pen ketika dua titik pertama berbagi koordinat y, dan tak ada apa pun yang menyalakannya kembali, sehingga setiap record berikutnya di file itu kehilangan outline-nya. Pen state itu milik EMR_SELECTOBJECT dan EMR_CREATEPEN; handler drawing record tak pantas mengubahnya. Special case itu dihapus di v3.539.41, dan bentuk satu titik dari record tak lagi membaca melewati titik-titiknya sendiri (diperbaiki di v3.539.39)
Titik PolyPolyline mulai setelah array counts
EMR_POLYPOLYLINE 32-bit menyimpan nPolys counts lalu cptl points, dan titik-titiknya mulai di byte offset 32 + nPolys * 4. Jebakannya ada di RTL: unit Windows mendeklarasikan TEMRPolyPolyline dengan aPolyCounts dan aptl sebagai array satu elemen, jadi aptl[0] adalah titik pertama hanya ketika nPolys bernilai 1. Code yang mengindeks aptl langsung akan membaca nilai count sebagai koordinat pada setiap record multi-line. Handler PDFlibPas lama juga mengukur bounds check-nya berdasarkan layout salah itu, sehingga record multi-line yang valid ditolak dan yang single-line tak menggambar apa pun. Sejak v3.539.41 PDFlibPas menemukan array titik dari offset hasil hitung, persis cara handler PolyPolygon-nya selalu melakukannya, dan menggambar tiap polyline sebagai subpath terbuka miliknya sendiri dengan satu stroke di akhir. Di v3.539.43 kembaran 16-bit-nya mendapat perlakuan yang sama; ia semula menggambar segmen demi segmen, yang merusak line join dan mengabaikan NULL_PEN terpilih
Pen dan brush default, serta path bracket
Dua aturan state melengkapi deretan perbaikan polyline di v3.539.43:
- GDI device context yang baru saja sudah memilih
BLACK_PENdanWHITE_BRUSH, sehingga metafile yang menggambar tanpaEMR_SELECTOBJECTapa pun tetap menggambar outline hitam; converter dulu mulai tanpa pen dan tanpa fill serta menulisn(end path, tak mengecat apa pun) untuk record seperti itu - Di dalam bracket
BeginPath/EndPath,Polylinetak memakai dan tak memperbarui current position, jadi ia harus membuka subpath baru di titik pertamanya alih-alih menyambung ke figure sebelumnya, dan tak boleh ada yang dicatat sampai bracket itu di-stroke atau di-fill
Membangun file test EMF dengan TMetafileCanvas
Cara tercepat menguji converter terhadap aturan-aturan ini adalah merekam tiga panggilan berisiko itu ke dalam satu enhanced metafile dengan TMetafileCanvas. Gambar di bawah merekam kurva-kurvanya dengan brush hollow lalu sengaja memilih brush solid kuning untuk polyline-nya: converter yang benar harus mengabaikan brush itu untuk polyline, jadi kemunculan warna kuning apa pun di PDF keluaran adalah bug. PolyDraw tak punya wrapper TCanvas, jadi ia dipanggil lewat Windows API dengan handle canvas, memakai type byte 3 dan 5 untuk menguji flag close-nya
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Persegi tertutup (3 = LINETO + CLOSEFIGURE), lalu satu figure Bezier
// tertutup yang triple control terakhirnya berakhir dengan 5 = BEZIERTO + CLOSEFIGURE
DrawPts: array[0..7] of TPoint = (
(X: 300; Y: 40), (X: 380; Y: 40), (X: 380; Y: 120), (X: 300; Y: 120),
(X: 420; Y: 120), (X: 440; Y: 40), (X: 520; Y: 40), (X: 540; Y: 120));
DrawTypes: array[0..7] of Byte = (
PT_MOVETO, PT_LINETO, PT_LINETO, PT_LINETO or PT_CLOSEFIGURE,
PT_MOVETO, PT_BEZIERTO, PT_BEZIERTO, PT_BEZIERTO or PT_CLOSEFIGURE);
var
Mf: TMetafile;
Canvas: TMetafileCanvas;
begin
Mf := TMetafile.Create;
try
Mf.Enhanced := True;
Mf.Width := 600;
Mf.Height := 260;
Canvas := TMetafileCanvas.Create(Mf, 0);
try
Canvas.Pen.Color := clNavy;
Canvas.Pen.Width := 2;
Canvas.Brush.Style := bsClear; // outline saja untuk kurva-kurvanya
// Titik 0 adalah start; 1..3 dan 4..6 adalah dua segmen cubic
Canvas.PolyBezier([Point(20, 120), Point(60, 20), Point(100, 220),
Point(140, 120), Point(180, 20), Point(220, 220), Point(260, 120)]);
PolyDraw(Canvas.Handle, DrawPts[0], DrawTypes[0], Length(DrawPts));
// Bentuk V terbuka dengan brush solid terpilih: di-stroke, tak pernah
// ditutup jadi segitiga kuning
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // mengakhiri perekaman
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Karena koordinat-koordinat ini muat di SmallInt, GDI biasanya akan menyimpan varian 16-bit-nya. Untuk sampai ke handler 32-bit, Anda butuh producer yang menulisnya, atau record yang Anda bangun sendiri. File buatan tangan membawa jebakannya sendiri: VCL TMetafile.LoadFromStream hanya memperlakukan stream sebagai EMF ketika sisa panjangnya lebih besar secara ketat dari TEnhMetaHeader 108 byte. EMF tulisan tangan minimal dengan header pendek, atau yang kosong dan panjangnya persis 108 byte, dianggap WMF dan ditolak dengan "Metafile is not valid". Selalu tulis header 108 byte lengkap, termasuk field ekstensinya, sebelum record test Anda
Mengimpor EMF ke PDF dengan PDFlibPas
PDFlibPas mengimpor EMF dengan ImportEMFFromFile atau ImportEMFFromStream, yang mengembalikan image ID non-nol saat sukses dan 0 saat gagal. GeneralOptions = 0 menjaga path vector yang jadi bahasan artikel ini; 1 merasterisasi metafile menjadi bitmap sebagai gantinya. FontOptions = 1 menambahkan font metafile sebagai font TrueType non-embedded. Varian stream me-rewind stream ke posisi 0 sebelum load, jadi berikan stream yang hanya berisi metafile itu
uses
System.SysUtils, PDFlibrary;
procedure EmfToPdf(const EmfFile, PdfFile: WideString);
var
PDF: TPDFlib;
ImageID: Integer;
PageOps: AnsiString;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1); // origin kiri atas untuk DrawImage
PDF.SetMeasurementUnits(0); // points
// FontOptions 1 = tambahkan font sebagai TrueType non-embedded
// GeneralOptions 0 = import vector, 1 = bitmap
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// Untuk EMF, ImageWidth / ImageHeight adalah ukuran frame dalam points
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// Halaman hanya memanggil form hasil impor: q ... cm /Name Do Q
PageOps := PDF.GetPageContentToString;
if Pos(AnsiString(' Do'), PageOps) = 0 then
raise Exception.Create('Expected a form XObject invocation');
if PDF.SaveToFile(PdfFile) <> 1 then
raise Exception.Create('The PDF could not be saved');
finally
PDF.Free;
end;
end;
Import EMF vector menjadi sebuah form XObject, jadi GetPageContentToString hanya mengembalikan urutan save, transform, Do, dan restore. Operator m, l, c, h, dan S yang dihasilkan dari record Poly* tinggal di form XObject stream yang terkompresi. Untuk mengauditnya, dekompres file yang tersimpan di PDF object inspector dan baca form stream-nya: untuk file test di atas Anda akan melihat polyline berakhir di S tanpa h sebelumnya, satu h di setiap flag close di figure-figure PolyDraw, dan tak ada f atau B di salah satu subpath itu. DrawImage juga menskalakan EMF impor secara seragam berdasarkan yang lebih kecil antara Width dan Height, sehingga gambar menjaga aspect ratio-nya walau box yang Anda berikan tidak cocok dengannya
Untuk target Free Pascal, lihat cara importer vector EMF PDFlibPas dibangun di bawah Free Pascal; semantik record-nya sama di mana pun importer itu ter-compile
Bagaimana parser EMF seharusnya memperlakukan jumlah titik dari file?
Parser EMF seharusnya memperlakukan setiap jumlah titik sebagai input tak terpercaya dan mengeceknya terhadap ukuran record sebelum menyalin satu titik pun. EnumEnhMetaFile hanya menjamin nSize tiap record tetap di dalam file. Ia tak mengecek bahwa cptl cocok dengan nSize, sehingga handler yang menyalin cptl titik dengan Move akan membaca record-record berikutnya, atau melewati ujung metafile, ketika count-nya dipalsukan atau korup. Sejak v3.539.39 PDFlibPas mengecek header tetap plus count dikali byte per titik terhadap nSize untuk PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo, dan Polygon di kedua lebar, dengan satu byte ekstra per titik untuk type byte PolyDraw. Untuk record PolyPoly, count per figure juga harus berjumlah tak lebih dari total yang dideklarasikan, dan figure tanpa titik dilewati
Pengecekan yang sama cukup pendek untuk disalin ke parser Anda sendiri. Versi ini memvalidasi EMR_POLYPOLYLINE 32-bit dan mengembalikan pointer ke array titik aslinya:
uses
Winapi.Windows;
// Mengembalikan nil kecuali record benar-benar memuat titik yang
// dideklarasikannya. Titik mulai setelah array counts: masuk 32 + nPolys * 4
// byte, bukan di aptl[0], yang RTL deklarasikan sebagai array satu elemen
function PolyPolylinePoints(Rec: PEnhMetaRecord): PPoint;
var
P: PEMRPolyPolyline;
Count: PDWORD;
PointsOffset, Total: Int64;
I: Cardinal;
begin
Result := nil;
if (Rec^.iType <> EMR_POLYPOLYLINE) or (Rec^.nSize < 32) then
Exit;
P := PEMRPolyPolyline(Rec);
if P^.nPolys = 0 then
Exit;
PointsOffset := 32 + Int64(P^.nPolys) * SizeOf(DWORD);
if PointsOffset + Int64(P^.cptl) * SizeOf(TPoint) > Rec^.nSize then
Exit; // count palsu atau terpotong
Total := 0;
Count := @P^.aPolyCounts[0]; // jalan dengan pointer: [0..0] memicu range check
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // figure mengklaim lebih banyak titik dari yang ada
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Test points-fit jalan lebih dulu, sehingga array counts diketahui berada di dalam record sebelum loop menelusurinya. Aritmetikanya Int64 karena nPolys * 4 dan cptl * 8 yang dihitung dalam 32 bit bisa wrap around dan lolos dari perbandingan
Referensi cepat: aturan Poly* EMF untuk konversi EMF ke PDF
EMR_POLYBEZIER: titik 0 adalah start point; kelompokkan dari titik 1 bertiga; diperbaiki untuk record 32-bit di v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: figure terbuka, stroke denganS, jangan pernahh,f, atauB, karena fill PDF menutup subpath terbukaEMR_POLYLINETO: mulai dari current position, tetap terbuka, perbarui current position, jangan pernah menyentuh pen stateEMR_POLYPOLYLINE32-bit: titik mulai di byte32 + nPolys * 4, bukan diaptl[0]EMR_POLYDRAW: maskPT_CLOSEFIGUREdulu sebelum dispatch, tutup setelah segmen selesai, mulai dari current position ketika titik pertama bukanPT_MOVETO- State device context default adalah
BLACK_PENplusWHITE_BRUSH; v3.539.43 dan seterusnya menghormatinya - Di dalam
BeginPath/EndPath, tiap polyline membuka subpath miliknya sendiri dan tak ada yang dicatat sampai bracket itu dipakai - Validasi setiap
cptl/cptsterhadapnSizedalam aritmetika 64-bit sebelum menyalin titik - EMF test buatan tangan butuh header 108 byte lengkap, atau
TMetafile.LoadFromStreammembacanya sebagai WMF
Kalau laporan Anda lewat komponen lain, semantik record yang sama tetap berlaku; import vector EMF dan WMF HotPDF membahas cara komponen itu mengubah brush gradient dan hatch menjadi pattern PDF, dan vector graphics, shaders dan gradients di PDFlibPas membahas menggambar bentuk yang sama langsung dengan API library alih-alih lewat metafile
PDFlibPas v3.539.43 atau lebih baru memuat semua aturan di atas. Detail dan unduhan trial ada di halaman produk library PDF Delphi PDFlibPas