Artikel Teknis

Import EMF PDFlibPas: Aturan PolyDraw, Polyline dan Bezier

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)

RecordMulai diTertutup?Current position
EMR_POLYBEZIERTitik 0TidakTak dipakai, tak diperbarui
EMR_POLYLINETitik 0Tidak (hanya pen)Tak dipakai, tak diperbarui
EMR_POLYLINETOCurrent positionTidak (hanya pen)Dipakai dan diperbarui
EMR_POLYPOLYLINETitik pertama tiap polylineTidak (hanya pen)Tak dipakai, tak diperbarui
EMR_POLYDRAWPT_MOVETO pertama, atau current positionHanya di mana PT_CLOSEFIGURE disetDipakai 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

Diagram PDFlibPas atas record EMR_POLYBEZIER dengan tujuh titik di mana titik nol membuka path dengan m dan titik satu sampai tiga serta empat sampai enam masing-masing membentuk satu segmen cubic c, mengontraskan handler 32-bit yang diperbaiki sejak v3.539.41 dengan pengelompokan lama yang menelan titik start sebagai control point
Titik 0 adalah start point dan hanya triple lengkap setelahnya yang menjadi segmen cubic, sehingga PolyBezier tujuh titik terrender sebagai m plus dua operator c

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_MOVETO di EMR_POLYDRAW16 16-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_MOVETO mulai dari current position, sesuai definisi record-nya, alih-alih menulis operator l atau c tanpa m pendahulu
Anatomi PDFlibPas atas type byte EMR_POLYDRAW di mana PT_CLOSEFIGURE adalah flag bit nol yang di-OR ke PT_LINETO atau PT_BEZIERTO, sehingga type byte valid 3 dan 5 harus dimask dengan and not PT_CLOSEFIGURE sebelum dispatch; case statement lama melewatkan kedua byte itu dan figure tertutup kehilangan sisi terakhirnya
Mask flag close-nya dulu sebelum dispatch dan terbitkan close hanya setelah garis atau triple Bezier selesai, atau PolyDraw diam-diam menjatuhkan titik

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

Perbandingan PDFlibPas atas polyline V terbuka yang di-export dari EMR_POLYLINE: converter yang benar mengakhiri path dengan stroke operator S dan mengabaikan brush terpilih, sedangkan menerbitkan f atau B menutup subpath terbuka secara implisit di bawah ISO 32000-1 8.5.3 dan mengecat bug chart wedge terisi
Fill operator menutup subpath terbuka mana pun sebelum mengecat, jadi polyline harus berakhir di S tanpa h, f, atau B di subpath itu

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_PEN dan WHITE_BRUSH, sehingga metafile yang menggambar tanpa EMR_SELECTOBJECT apa pun tetap menggambar outline hitam; converter dulu mulai tanpa pen dan tanpa fill serta menulis n (end path, tak mengecat apa pun) untuk record seperti itu
  • Di dalam bracket BeginPath / EndPath, Polyline tak 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.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: figure terbuka, stroke dengan S, jangan pernah h, f, atau B, karena fill PDF menutup subpath terbuka
  • EMR_POLYLINETO: mulai dari current position, tetap terbuka, perbarui current position, jangan pernah menyentuh pen state
  • EMR_POLYPOLYLINE 32-bit: titik mulai di byte 32 + nPolys * 4, bukan di aptl[0]
  • EMR_POLYDRAW: mask PT_CLOSEFIGURE dulu sebelum dispatch, tutup setelah segmen selesai, mulai dari current position ketika titik pertama bukan PT_MOVETO
  • State device context default adalah BLACK_PEN plus WHITE_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 / cpts terhadap nSize dalam aritmetika 64-bit sebelum menyalin titik
  • EMF test buatan tangan butuh header 108 byte lengkap, atau TMetafile.LoadFromStream membacanya 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