Teknik Makale

PDFlibPas EMF: PolyDraw, Polyline ve Bezier kuralları

Delphi için losLab PDF kütüphanesi olan PDFlibPas, EMF Poly* kayıtlarını [MS-EMF]'teki her kayıt tanımına uyarak PDF yollarına çevirir: 32 bitlik EMR_POLYBEZIER 0. noktadan başlar, polyline'lar açık kalır ve yalnızca stroke edilir, EMR_POLYDRAW'daki PT_CLOSEFIGURE bir bayraktır ve her nokta sayısı kayıt boyutuyla karşılaştırılarak denetlenir. Bu kurallar v3.539.39, v3.539.41 ve v3.539.43 sürümlerine dağıldı. Ondan önce, bir rapor grafiği ImportEMFFromFile'dan trend çizgisinin olması gereken yerde dolu bir dilim, yanlış kontrol noktasına kıvrılan bir Bezier eğrisi ya da son kenarı eksik kapanmış bir kontur ile çıkabilirdi. Bunların hiçbiri hata üretmedi ve kurallar her Delphi EMF-to-PDF dönüştürücüsü ile GDI kayıt ayrıştırıcısı için geçerli

EMF Poly* kayıtları PDF dönüşümünde neden yanlış gider?

EMF Poly* kayıtları yanlış gider, çünkü her biri anlamının bir kısmını noktalarının dışında taşır: şeklin açık olup olmadığı, geçerli konumdan başlayıp başlamadığı, hangi kalem ve fırçanın geçerli olduğu ve noktaların kayıt içinde nerede başladığı. Bir enhanced metafile, bir device context'e karşı yapılan GDI çağrılarının kaydıdır; dolayısıyla dönüştürücünün koordinatların yanı sıra o device context durumunu da yeniden oynatması gerekir. PDF'in device context'i yoktur. Elinde bir path, o pathin içinde bir current point ve stroke (S), fill (f) ile ikisi (B) arasında karar veren bir boyama operatörü vardır. İki model arasındaki her uyumsuzluk sessiz bir render farkına dönüşür

Poly* ailesi iki genişlikte de gelir. EMR_POLYLINE gibi her 32 bitlik kaydın, noktaları SmallInt çiftleri olarak saklayan EMR_POLYLINE16 gibi 16 bitlik bir ikizi vardır. GDI genellikle tüm koordinatlar sığdığında kompakt biçimi kaydeder; böylece bir dönüştürücünün 32 bitlik işleyicileri, günlük test çizimleri onlara hiç ulaşmadığı için yıllarca yanlış kalabilir. En hızlı denetim, aynı noktaları iki kayıttan geçirmek ve ortaya çıkan yolları karşılaştırmaktır. Burada ele alınan kayıtların hepsi [MS-EMF]'in çizim kayıt grubundadır (2.3.5 Drawing Record Types)

KayıtBaşlangıçKapalı mı?Geçerli konum
EMR_POLYBEZIER0. noktaHayırKullanılmaz, güncellenmez
EMR_POLYLINE0. noktaHayır (yalnızca kalem)Kullanılmaz, güncellenmez
EMR_POLYLINETOGeçerli konumHayır (yalnızca kalem)Kullanılır ve güncellenir
EMR_POLYPOLYLINEHer polyline'ın ilk noktasıHayır (yalnızca kalem)Kullanılmaz, güncellenmez
EMR_POLYDRAWİlk PT_MOVETO ya da geçerli konumYalnızca PT_CLOSEFIGURE setli yerlerdeKullanılır ve güncellenir

Bir EMR_POLYBEZIER eğrisi aslında nereden başlar?

Bir EMR_POLYBEZIER eğrisi 0. noktadan başlar ve yalnızca 1. indeksten itibaren noktalar kontrol noktası, kontrol noktası, bitiş noktası biçiminde üçlü gruplanır. 7 noktalı bir kayıt bu yüzden iki kübik segment çizer: 0 başlangıçtır, 1-3 ilk segmenti, 4-6 ikinciyi oluşturur. PDFlibPas'taki 16 bitlik işleyici bunu zaten yapıyordu. 32 bitlik işleyici gruplamaya 0. noktadan başlıyordu; böylece başlangıç noktası ilk kontrol noktası olarak tüketiliyordu ve sonraki her segment bir kayıyordu. Eğri yine de render ediliyordu, sadece yanlışı. v3.539.41'den beri her iki genişlik de pathi 0. noktada m ile açar ve ardından her tam üçlü için bir c üretir

Yedi noktalı bir EMR_POLYBEZIER kaydının PDFlibPas şeması: 0. nokta pathi m ile açar, 1-3 ve 4-6 noktalarının her biri bir kübik c segmenti oluşturur; v3.539.41'den beri düzeltilmiş 32 bitlik işleyici, başlangıç noktasını kontrol noktası olarak tüketen eski gruplamayla karşılaştırılır
0. nokta başlangıç noktasıdır ve yalnızca ardından gelen tam üçlüler kübik segmente dönüşür; yedi noktalı bir PolyBezier, m artı iki c operatörü olarak render edilir

Kendi ayrıştırıcınız için: 1 artı 3'ün katı olmayan bir nokta sayısı bozuktur ve sondaki noktalar bir eğriye dikilmek yerine yok sayılmalıdır

PolyDraw: PT_CLOSEFIGURE bayraktır, nokta tipi değil

EMR_POLYDRAW'da PT_CLOSEFIGURE (değeri 1), PT_LINETO (2) ya da PT_BEZIERTO (4) ile birleştirilen bir bittir; dolayısıyla geçerli bir tip baytı 3 ya da 5 olabilir. Nokta tipi, o bit maskelenmiş hâliyle baytın kendisidir ve bayrak, bu noktada biten segmentin ardından şekli kapat anlamına gelir. Eski PDFlibPas işleyicisi baytı bir case deyiminde tek değerlerle eşliyordu; böylece 3 ve 5 tipli noktalar hiçbir şeyle eşleşmedi ve tamamen atlandı. PolyDraw ile çizilen bir dikdörtgen kapanış kenarını kaybediyor, bayrağı taşıyan son noktalı bir Bezier üçlüsü o noktayı kaybediyordu ve bu da sonraki her üçlüyü kaydırıyordu

v3.539.39'dan beri tip, Types[i] and not PT_CLOSEFIGURE olarak okunuyor ve kapatma yalnızca tam bir segmentin ardından üretiliyor: kapanan bir PT_LINETO için çizginin ardından, bir Bezier grubunun üçüncü noktasının ardından. Bir üçlünün ilk ya da ikinci noktasına bayrak set eden bozuk bir dosya şekli erken kapatmaz. Aynı sürümde iki ilişkili düzeltme daha çıktı:

  • 16 bitlik EMR_POLYDRAW16'daki her PT_MOVETO tüm pathi yeniden başlatıyordu; böylece üç şekil taşıyan bir kayıt yalnızca sonuncuyu tutuyordu. Artık ilk hareket pathi başlatıyor, sonraki hareketler subpath açıyor
  • PT_MOVETO ile başlamayan bir PolyDraw kaydı, önünde m olmayan bir l ya da c operatörü yazmak yerine kayıt tanımının söylediği gibi geçerli konumdan başlar
Bir EMR_POLYDRAW tip baytının PDFlibPas anatomisi: PT_CLOSEFIGURE, PT_LINETO ya da PT_BEZIERTO içine OR'lanmış sıfır numaralı bayrak bittir; dolayısıyla geçerli 3 ve 5 tip baytları dağıtımdan önce and not PT_CLOSEFIGURE ile maskelenmelidir. Eski case deyimi her iki baytı da atlıyor ve kapanan şekiller son kenarlarını kaybediyordu
Kapatma bayrağını dağıtımdan önce maskeyin ve kapatmayı yalnızca tamamlanmış bir çizginin ya da Bezier üçlüsünün ardından üretin, yoksa PolyDraw noktaları sessizce düşürür

Bir EMF polyline neden PDF'te asla doldurulmamalı?

Bir EMF polyline asla doldurulmamalıdır, çünkü EMR_POLYLINE ve EMR_POLYPOLYLINE yalnızca kalemle çizilen açık şekillerdir ve PDF'te açık bir pathi doldurmak onu dolaylı olarak kapatır. ISO 32000-1 §8.5.3, fill operatörlerinin boyamadan önce her açık subpath'i kapattığını söyler. Üç noktalı bir polyline için B ya da f üreten bir dönüştürücü bu yüzden geçerli fırça renginde dolu bir üçgen boyar: grafik trend çizgisi altındaki dolu dilim, yani. v3.539.41 öncesinde PDFlibPas her iki polyline genişliğini de fırçayla dolduruyor ve 32 bitlik kayıt ayrıca açıkça kapatılıyordu. Bugün her iki genişlik de yalnızca stroke ile bitiyor ve GDI ayrımı korunuyor: Polygon kapatır ve doldurur, Polyline asla yapmaz

EMR_POLYLINE'dan dışa aktarılan açık bir V polyline'ının PDFlibPas karşılaştırması: doğru dönüştürücü pathi stroke operatörü S ile bitirir ve seçili fırçayı yok sayar; f ya da B üretmek ise ISO 32000-1 8.5.3 uyarınca açık subpath'i dolaylı olarak kapatır ve dolu dilim grafik hatasını boyar
Bir fill operatörü boyamadan önce her açık subpath'i kapatır; dolayısıyla polyline'lar S ile bitmeli ve subpath'te h, f ya da B bulunmamalıdır

PolylineTo geçerli konumdan başlar

EMR_POLYLINETO geçerli konumdan kayıttaki her noktaya doğru çizer, açık kalır ve geçerli konumu son noktada bırakır. Eski işleyici ayrıca ilk iki nokta aynı y koordinatını paylaştığında kalemi kapatan bir özel durum içeriyordu ve onu hiçbir şey geri açmıyordu; böylece dosyadaki sonraki her kayıt konturunu kaybediyordu. Kalem durumu EMR_SELECTOBJECT ve EMR_CREATEPEN'e aittir; bir çizim kaydı işleyicisinin onu değiştirmeye işi yoktur. O özel durum v3.539.41'de kaldırıldı ve kaydın tek noktalı biçimi artık kendi noktalarının ötesini okumuyor (v3.539.39'da düzeltildi)

PolyPolyline noktaları counts dizisinin ardından başlar

32 bitlik EMR_POLYPOLYLINE önce nPolys adet sayı sonra cptl nokta saklar ve noktalar 32 + nPolys * 4 bayt ofsetinde başlar. Tuzak RTL'dedir: Windows uniti, TEMRPolyPolyline'ı aPolyCounts ile aptl'i tek elemanlı diziler olarak bildirir; dolayısıyla aptl[0], yalnızca nPolys 1 olduğunda ilk noktadır. aptl'i doğrudan indeksleyen kod, her çoklu çizgi kaydında sayı değerlerini koordinat olarak okur. Eski PDFlibPas işleyicisi sınır denetimini de o yanlış düzene göre boyutlandırıyordu; böylece geçerli çoklu çizgi kayıtları reddediliyor, tek çizgililer hiçbir şey çizmiyordu. v3.539.41'den beri PDFlibPas nokta dizisini hesaplanan ofsetten bulur — PolyPolygon işleyicisinin her zaman yaptığı gibi — ve her polyline'ı sonunda tek bir stroke ile kendi açık subpath'i olarak çizer. v3.539.43'te 16 bitlik ikiz de aynı muameleyi gördü; o ise segment segment çiziyordu, bu da line join'leri bozuyor ve seçili bir NULL_PEN'i yok sayıyordu

Varsayılan kalem ve fırça, path parantezleri

v3.539.43'teki polyline düzeltmelerini iki durum kuralı tamamlar:

  • Yeni bir GDI device context'inde zaten BLACK_PEN ve WHITE_BRUSH seçilidir; dolayısıyla hiçbir EMR_SELECTOBJECT olmadan çizen bir metafile yine de siyah konturlar çizer. Dönüştürücü ise kalemsiz ve dolgusuz başlıyor ve bu tür kayıtlar için n (pathi bitir, hiçbir şey boyama) yazıyordu
  • Bir BeginPath / EndPath parantezi içinde bir Polyline geçerli konumu ne kullanır ne günceller; dolayısıyla önceki şekle bağlanmak yerine ilk noktasında yeni bir subpath açmalı ve parantez stroke edilene ya da doldurulana dek hiçbir şey boyanmamalıdır

TMetafileCanvas ile bir EMF test dosyası oluşturmak

Bir dönüştürücüyü bu kurallara karşı denemenin en hızlı yolu, üç riskli çağrıyı TMetafileCanvas ile tek bir enhanced metafile içine kaydetmektir. Aşağıdaki çizim eğrileri içi boş bir fırçayla kaydediyor ve ardından polyline için kasıtlı olarak dolu sarı bir fırça seçiyor: doğru dönüştürücü o fırçayı polyline için yok saymalıdır, yani çıktı PDF'teki her sarı bir hatadır. PolyDraw'un TCanvas sarmalayıcısı yoktur; canvas handle'ı üzerinden Windows API'siyle çağrılır ve kapatma bayrağını yoklamak için 3 ve 5 tip baytları kullanılır

uses
  Winapi.Windows, System.Types, Vcl.Graphics;

procedure BuildPolyTestEmf(const FileName: string);
const
  // Kapalı bir kare (3 = LINETO + CLOSEFIGURE), ardından son kontrol üçlüsü
  // 5 = BEZIERTO + CLOSEFIGURE ile biten kapalı bir Bezier şekli
  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;    // eğriler için yalnızca konturlar
      // 0. nokta başlangıçtır; 1..3 ve 4..6 iki kübik segmenttir
      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));
      // Dolu fırça seçiliyken açık V şekli: stroke edilir, asla sarı bir
      // üçgene kapatılmaz
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // kaydı bitirir
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Bu koordinatlar bir SmallInt'e sığdığı için GDI normalde 16 bitlik varyantları saklar. 32 bitlik işleyicilere ulaşmak için onları yazan bir üreticiye ya da elle kurduğunuz kayıtlara ihtiyacınız vardır. El yapımı dosyaların kendi tuzağı vardır: VCL TMetafile.LoadFromStream, akışı yalnızca kalan uzunluk 108 baytlık TEnhMetaHeader'dan kesinlikle büyük olduğunda EMF sayar. Kısa başlıklı elle yazılmış minimal bir EMF ya da tam 108 bayt uzunluğundaki boş bir dosya WMF sanılır ve "Metafile is not valid" ile reddedilir. Test kayıtlarınızdan önce uzantı alanları dahil tam 108 baytlık başlığı daima yazın

EMF'i PDFlibPas ile bir PDF'e içe aktarmak

PDFlibPas bir EMF'i ImportEMFFromFile ya da ImportEMFFromStream ile içe aktarır; ikisi de başarıda sıfır olmayan bir image ID, başarısızlıkta 0 döndürür. GeneralOptions = 0, bu yazının konusu olan vector pathi tutar; 1, metafile'ı bunun yerine bir bitmap'e rasterleştirir. FontOptions = 1, metafile fontlarını non-embedded TrueType fontlar olarak ekler. Stream varyantı yüklemeden önce akışı 0 konumuna geri sarar; dolayısıyla yalnızca metafile'ı içeren bir stream geçirin

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);              // DrawImage için sol üst köken
    PDF.SetMeasurementUnits(0);    // point
    // FontOptions 1 = fontları non-embedded TrueType olarak ekler
    // GeneralOptions 0 = vector içe aktarma, 1 = bitmap
    ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
    if ImageID = 0 then
      raise Exception.Create('The metafile could not be imported');
    PDF.SelectImage(ImageID);
    // Bir EMF için ImageWidth / ImageHeight point cinsinden çerçeve boyutudur
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // Sayfa yalnızca içe aktarılan formu çağırır: 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;

Vector bir EMF içe aktarması bir form XObject'e dönüşür; dolayısıyla GetPageContentToString yalnızca save, transform, Do ve restore dizisini döndürür. Poly* kayıtlarından üretilen m, l, c, h ve S operatörleri sıkıştırılmış olan form XObject streaminde yaşar. Onları denetlemek için kaydedilen dosyayı bir PDF object inspector'da açıp form streamini okuyun: yukarıdaki test dosyası için polyline'ın önünde h olmadan S ile bittiğini, PolyDraw şekillerindeki her kapatma bayrağında bir h olduğunu ve bu subpath'lerin hiçbirinde f ya da B bulunmadığını görmelisiniz. DrawImage ayrıca içe aktarılan bir EMF'i Width ile Height'ın küçüğü ölçüsünde tek düze ölçekler; yani geçirdiğiniz kutu orana uymasa bile çizim en-boy oranını korur

Free Pascal hedefleri için PDFlibPas EMF vector içe aktarıcısının Free Pascal altında derlenmesi yazısına bakın; kayıt semantiği içe aktarıcının derlendiği her yerde aynıdır

Bir EMF ayrıştırıcısı dosyadan gelen nokta sayılarına nasıl davranmalı?

Bir EMF ayrıştırıcısı her nokta sayısını güvenilmeyen girdi saymalı ve tek bir nokta kopyalamadan önce kayıt boyutuyla denetlemelidir. EnumEnhMetaFile yalnızca her kaydın nSize'ının dosya içinde kaldığını garanti eder. cptl ile nSize'ın uyduğunu denetlemez; dolayısıyla cptl noktayı Move ile kopyalayan bir işleyici, sayı uydurulmuş ya da bozulmuş olduğunda izleyen kayıtları hatta metafile'ın sonunu okur. v3.539.39'dan beri PDFlibPas, PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo ve Polygon için sabit başlık artı nokta başına bayt sayısı çarpı sayı toplamını her iki genişlikte nSize ile karşılaştırır; PolyDraw tip baytları için nokta başına bir bayt fazladır. PolyPoly kayıtlarında şekil başına sayıların ayrıca bildirilen toplamı aşmaması gerekir ve sıfır noktalı şekiller atlanır

Aynı denetim kendi ayrıştırıcınıza kopyalanacak kadar kısadır. Bu sürüm 32 bitlik bir EMR_POLYPOLYLINE doğrular ve gerçek nokta dizisine bir işaretçi döndürür:

uses
  Winapi.Windows;

// Kayıt gerçekte bildirdiği noktaları tutmadıkça nil döndürür.
// Noktalar counts dizisinin ardından başlar: 32 + nPolys * 4 bayt içeride,
// RTL'nin tek elemanlı dizi olarak bildirdiği aptl[0] değil
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;                         // sahte ya da kesilmiş sayı
  Total := 0;
  Count := @P^.aPolyCounts[0];    // işaretçiyle yürü: [0..0] aralık denetimini tetikler
  for I := 1 to P^.nPolys do
  begin
    Inc(Total, Count^);
    Inc(Count);
  end;
  if Total > P^.cptl then
    Exit;                         // şekiller var olandan çok nokta iddia ediyor
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

Nokta-sığma testi önce koşar; dolayısıyla döngü gezinmeden önce counts dizisinin kayıt içinde olduğu bilinir. Aritmetik Int64'tür, çünkü 32 bitte hesaplanan nPolys * 4 ve cptl * 8 taşabilir ve karşılaştırmayı geçebilir

Hızlı başvuru: EMF-to-PDF dönüşümü için EMF Poly* kuralları

  • EMR_POLYBEZIER: 0. nokta başlangıç noktasıdır; 1. noktadan itibaren üçlü gruplayın; 32 bitlik kayıt için v3.539.41'de düzeltildi
  • EMR_POLYLINE / EMR_POLYPOLYLINE: açık şekiller, S ile stroke; asla h, f ya da B, çünkü PDF fill açık subpath'leri kapatır
  • EMR_POLYLINETO: geçerli konumdan başla, açık kal, geçerli konumu güncelle, kalem durumuna asla dokunma
  • 32 bitlik EMR_POLYPOLYLINE: noktalar 32 + nPolys * 4 baytında başlar, aptl[0]'da değil
  • EMR_POLYDRAW: dağıtımdan önce PT_CLOSEFIGURE'ü maskeleyin, tamamlanan segmentin ardından kapatın, ilk nokta PT_MOVETO değilse geçerli konumdan başlayın
  • Varsayılan device context durumu BLACK_PEN artı WHITE_BRUSH'tır; v3.539.43 ve sonrası buna uyar
  • BeginPath / EndPath içinde her polyline kendi subpath'ini açar ve parantez kullanılana dek hiçbir şey boyanmaz
  • Noktaları kopyalamadan önce her cptl / cpts değerini nSize ile 64 bit aritmetikte doğrulayın
  • El yapımı test EMF'leri tam 108 baytlık başlık ister, yoksa TMetafile.LoadFromStream onları WMF okur

Raporlarınız başka bir bileşenden geçiyorsa aynı kayıt semantiği geçerlidir; HotPDF EMF ve WMF vector içe aktarma o bileşenin gradient ve hatch fırçalarını PDF pattern'lerine nasıl çevirdiğini kapsar, PDFlibPas'ta vector grafik, shader ve gradientler ise aynı şekilleri metafile üzerinden değil doğrudan kütüphane API'siyle çizmeyi anlatır

PDFlibPas v3.539.43 ve sonrası yukarıdaki her kuralı içerir. Ayrıntılar ve deneme indirmeleri PDFlibPas Delphi PDF library ürün sayfasındadır