PDFlibPas، کتابخانهٔ PDF losLab برای Delphi، رکوردهای Poly* را در EMF با پیروی از تعریف هر رکورد در [MS-EMF] به pathهای PDF تبدیل میکند: EMR_POLYBEZIER سی و دو بیتی از نقطهٔ 0 شروع میشود، polylineها باز میمانند و فقط stroke میشوند، PT_CLOSEFIGURE در EMR_POLYDRAW یک فلگ است، و هر شمارندهٔ نقطه نسبت به اندازهٔ رکورد چک میشود. این قواعد در v3.539.39 و v3.539.41 و v3.539.43 فرود آمدند. قبل از آنها ممکن بود یک چارت گزارشی از ImportEMFFromFile بیرون بیاید با یک گوهٔ پرشده بهجای خط روند، یک منحنی Bezier که به control point اشتباهی خم شده بود، یا یک کانتور بسته که ضلع آخرش غایب بود. هیچکدام از اینها error بالا نمیدادند، و این قواعد برای هر converter ای از EMF به PDF در Delphi یا هر parser رکوردهای GDI هم صدق میکنند
چرا رکوردهای Poly* در EMF موقع تبدیل به PDF خراب میشوند؟
رکوردهای Poly* در EMF برای این خراب میشوند که هر کدام بخشی از معنای خود را بیرون از نقاطش حمل میکند: اینکه شکل باز است یا نه، اینکه از موقعیت فعلی شروع میشود یا نه، کدام pen و brush اعمال میشوند، و نقاط از کجای رکورد آغاز میشوند. یک enhanced metafile ضبطی از فراخوانیهای GDI روی یک device context است، پس converter باید علاوه بر مختصات، state آن device context را هم دوباره اجرا کند. PDF هیچ device context ای ندارد. یک path دارد، یک current point داخل آن path، و یک عملگر نقاشی که بین stroke (S) و fill (f) و هر دو (B) تصمیم میگیرد. هر ناهماهنگی بین این دو مدل به یک تفاوت بیصدا در رندر تبدیل میشود
خانوادهٔ Poly* در دو عرض عرضه میشود. هر رکورد 32 بیتی مثل EMR_POLYLINE یک دوقلوی 16 بیتی مثل EMR_POLYLINE16 دارد که نقاط را بهشکل جفتهای SmallInt ذخیره میکند. GDI معمولاً وقتی همهٔ مختصات جا شوند نسخهٔ فشرده را ضبط میکند، پس handlerهای 32 بیتی یک converter میتوانند سالها خراب بمانند در حالی که نقشههای تست روزمره هرگز به آنها نمیرسند. سریعترین ممیزی این است که همان نقاط را از هر دو رکورد عبور دهی و pathهای حاصل را مقایسه کنی. رکوردهای این مقاله همه در گروه رکوردهای رسم [MS-EMF] هستند (2.3.5 Drawing Record Types)
| رکورد | شروع از | بسته؟ | موقعیت فعلی |
|---|---|---|---|
EMR_POLYBEZIER | نقطهٔ 0 | خیر | استفاده نمیشود، بهروز نمیشود |
EMR_POLYLINE | نقطهٔ 0 | خیر (فقط pen) | استفاده نمیشود، بهروز نمیشود |
EMR_POLYLINETO | موقعیت فعلی | خیر (فقط pen) | استفاده و بهروز میشود |
EMR_POLYPOLYLINE | نقطهٔ اول هر polyline | خیر (فقط pen) | استفاده نمیشود، بهروز نمیشود |
EMR_POLYDRAW | اولین PT_MOVETO، یا موقعیت فعلی | فقط هر جا که PT_CLOSEFIGURE ست شده باشد | استفاده و بهروز میشود |
منحنی EMR_POLYBEZIER واقعاً از کجا شروع میشود؟
یک منحنی EMR_POLYBEZIER از نقطهٔ 0 شروع میشود و فقط نقاط از اندیس 1 به بعد سهتایی گروه میشوند: control point، control point، نقطهٔ پایان. پس رکوردی با 7 نقطه دو سگمنت مکعبی میکشد: نقطهٔ 0 شروع است، 1 تا 3 سگمنت اول را میسازند و 4 تا 6 سگمنت دوم. handler 16 بیتی در PDFlibPas همین را از قبل درست انجام میداد. handler 32 بیتی گروهبندی را از نقطهٔ 0 شروع میکرد، پس نقطهٔ شروع بهعنوان اولین control point مصرف میشد و هر سگمنت بعدی یکی جابهجا بود. منحنی همچنان رندر میشد، فقط منحنی اشتباه. از v3.539.41 هر دو عرض path را با m روی نقطهٔ 0 باز میکنند و بعد از آن بهازای هر سهتایی کامل یک c صادر میکنند
برای parser خودت: شمارندهای که 1 بهعلاوهٔ مضربی از 3 نیست malformed است و نقاط اضافی انتهایی باید نادیده گرفته شوند نه اینکه به زور به منحنی دوخته شوند
PolyDraw: PT_CLOSEFIGURE یک فلگ است نه یک نوع نقطه
در EMR_POLYDRAW، PT_CLOSEFIGURE (مقدار 1) بتی است که با PT_LINETO (2) یا PT_BEZIERTO (4) ترکیب میشود، پس یک بایت نوع معتبر میتواند 3 یا 5 باشد. نوع نقطه همان بایت است با آن بیت ماسکشده، و فلگ یعنی شکل را بعد از سگمنتی که روی این نقطه تمام میشود ببند. handler قدیمی PDFlibPas بایت را در یک دستور case با مقادیر تکی مقایسه میکرد، پس نقاطی از نوع 3 و 5 به هیچچیز نمیخوردند و کلاً رد میشدند. یک مستطیلی که با PolyDraw کشیده میشد ضلع بستنش را گم میکرد، و یک سهتایی Bezier که نقطهٔ آخرش فلگ را داشت همان نقطه را گم میکرد، که هر سهتایی بعدی را از ریتم میانداخت
از v3.539.39 نوع بهشکل Types[i] and not PT_CLOSEFIGURE خوانده میشود و بستن فقط بعد از یک سگمنت کامل صادر میشود: بعد از خط برای یک PT_LINETO بستهشونده، و بعد از نقطهٔ سوم یک گروه Bezier. فایل malformed ای که فلگ را روی نقطهٔ اول یا دوم یک سهتایی ست میکند شکل را زودتر نمیبندد. دو فیکس مرتبط هم در همان release ارسال شد:
- هر
PT_MOVETOدرEMR_POLYDRAW1616 بیتی کل path را از نو شروع میکرد، پس رکوردی با سه شکل فقط آخری را نگه میداشت؛ حالا حرکت اول path را آغاز میکند و حرکتهای بعدی subpath باز میکنند - رکورد PolyDraw ای که با
PT_MOVETOشروع نمیشود از موقعیت فعلی آغاز میشود، همانطور که تعریف رکورد میگوید، بهجای آنکه یک عملگرlیاcبدونmقبلی بنویسد
چرا polyline در EMF هرگز نباید در PDF پر شود؟
یک polyline در EMF هرگز نباید پر شود چون EMR_POLYLINE و EMR_POLYPOLYLINE شکلهای بازی هستند که فقط با pen کشیده میشوند و پر کردن یک path باز در PDF آن را ضمنی میبندد. ISO 32000-1 §8.5.3 میگوید عملگرهای fill هر subpath بازی را قبل از نقاشی میبندند. پس converter ای که برای یک polyline سهنقطهای B یا f صادر میکند یک مثلث پرشده به رنگ brush فعلی نقاشی میکند: همان گوهٔ پرشده زیر خط روند چارت. قبل از v3.539.41، PDFlibPas هر دو عرض polyline را با brush پر میکرد و رکورد 32 بیتی هم صریحاً بسته میشد. حالا هر دو عرض فقط با stroke تمام میشوند و تمایز GDI حفظ میشود: Polygon میبندد و پر میکند، Polyline هرگز
PolylineTo از موقعیت فعلی شروع میشود
EMR_POLYLINETO از موقعیت فعلی از میان هر نقطهٔ رکورد میکشد، باز میماند و موقعیت فعلی را روی آخرین نقطه میگذارد. handler قدیمی یک حالت خاص هم داشت که وقتی دو نقطهٔ اول y مشترک داشتند pen را خاموش میکرد، و هیچچیز هرگز دوباره روشنش نمیکرد، پس هر رکورد بعدی در فایل کانتورش را از دست میداد. وضعیت pen به EMR_SELECTOBJECT و EMR_CREATEPEN تعلق دارد؛ handler یک رکورد رسم هیچ کاری به آن ندارد. آن حالت خاص در v3.539.41 حذف شد، و فرم تکنقطهای رکورد هم دیگر از نقاط خودش بیرون نمیخواند (فیکس در v3.539.39)
نقاط PolyPolyline بعد از آرایهٔ شمارندهها شروع میشوند
EMR_POLYPOLYLINE 32 بیتی nPolys شمارنده و بعد cptl نقطه ذخیره میکند و نقاط از بایت آفست 32 + nPolys * 4 آغاز میشوند. تله داخل RTL است: یونیت Windows TEMRPolyPolyline را با aPolyCounts و aptl بهشکل آرایههای تکعنصری declare میکند، پس aptl[0] فقط وقتی nPolys برابر 1 است اولین نقطه است. کدی که مستقیم روی aptl اندیس میزند مقدارهای شمارنده را بهعنوان مختصات میخواند برای هر رکورد چندخطی. handler قدیمی PDFlibPas هم چک محدودهاش را روی همان layout غلط اندازه گرفته بود، پس رکوردهای چندخطی معتبر رد میشدند و تکخطیها هیچ نمیکشیدند. از v3.539.41 PDFlibPas آرایهٔ نقاط را از آفست محاسبهشده پیدا میکند، همانطور که handler PolyPolygon آن همیشه میکرد، و هر polyline را بهعنوان subpath باز خودش با یک stroke در انتها میکشد. در v3.539.43 دوقلوی 16 بیتی هم همین درمان را گرفت؛ آن یکی سگمنتبهسگمنت میکشید که line joinها را میشکست و یک NULL_PEN انتخابشده را نادیده میگرفت
pen و brush پیشفرض، و براکتهای path
دو قاعدهٔ state فیکسهای polyline در v3.539.43 را کامل میکنند:
- یک device context تازهٔ GDI از قبل
BLACK_PENوWHITE_BRUSHانتخابشده دارد، پس متافیلی که بدون هیچEMR_SELECTOBJECTای میکشد همچنان کانتورهای سیاه میکشد؛ converter قبلاً بدون pen و بدون fill شروع میکرد و برای چنین رکوردهاییnمینوشت (پایان path، بدون نقاشی) - داخل براکت
BeginPath/ EndPath، یکPolylineنه از موقعیت فعلی استفاده میکند نه آن را بهروز میکند، پس باید در اولین نقطهاش subpath جدید باز کند بهجای اتصال به شکل قبلی، و تا وقتی براکت stroke یا fill نشده هیچچیز نباید نقاشی شود
ساختن فایل تست EMF با TMetafileCanvas
سریعترین راه برای چک کردن یک converter در برابر این قواعد این است که سه فراخوانی پرریسک را با TMetafileCanvas در یک enhanced metafile ضبط کنی. رسم زیر منحنیها را با یک brush توخالی ضبط میکند و بعد عمداً یک brush زرد توپر برای polyline انتخاب میکند: converter درست باید برای polyline آن brush را نادیده بگیرد، پس هر زردی در PDF خروجی یک باگ است. PolyDraw هیچ wrapper ای در TCanvas ندارد، پس از طریق Windows API با handle بوم صدا زده میشود، با بایتهای نوع 3 و 5 تا فلگ بستن ورزیده شود
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// یک مربع بسته (3 = LINETO + CLOSEFIGURE)، بعد یک شکل Bezier بسته
// که سهتایی کنترلی آخرش با 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; // فقط کانتور برای منحنیها
// نقطهٔ 0 شروع است؛ 1..3 و 4..6 دو سگمنت مکعبیاند
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));
// شکل V باز با brush توپر انتخابشده: فقط stroke میشود، هرگز
// به مثلث زرد بسته نمیشود
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // ضبط را تمام میکند
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
چون این مختصات در یک SmallInt جا میشوند، GDI معمولاً واریانتهای 16 بیتی را ذخیره میکند. برای رسیدن به handlerهای 32 بیتی یا باید تولیدکنندهای داشته باشی که آنها را بنویسد، یا رکوردها را دستی بسازی. فایلهای دستساز تلهٔ خودشان را دارند: TMetafile.LoadFromStream در VCL استریم را فقط وقتی EMF میداند که طول باقیمانده اکیداً از TEnhMetaHeader 108 بایتی بیشتر باشد. یک EMF دستنویس حداقلی با header کوتاه، یا یک EMF خالی که دقیقاً 108 بایت است، WMF فرض میشود و با «Metafile is not valid» رد میشود. همیشه قبل از رکوردهای تستت header کامل 108 بایتی را با فیلدهای extension بنویس
وارد کردن EMF به PDF با PDFlibPas
PDFlibPas یک EMF را با ImportEMFFromFile یا ImportEMFFromStream وارد میکند که روی موفقیت یک image ID غیرصفر و روی شکست 0 برمیگردانند. GeneralOptions = 0 همان path برداری را نگه میدارد که موضوع این مقاله است؛ 1 متافایل را بهجای آن به بیتمپ rasterize میکند. FontOptions = 1 فونتهای متافایل را بهشکل فونتهای TrueType غیرجاسازیشده اضافه میکند. واریانت استریم قبل از load استریم را به موقعیت 0 برمیگرداند، پس استریمی بده که فقط متافایل را داشته باشد
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
PDF.SetMeasurementUnits(0); // بر حسب point
// FontOptions 1 = اضافه کردن فونتها بهشکل TrueType غیرجاسازیشده
// GeneralOptions 0 = import برداری، 1 = بیتمپ
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// برای یک EMF، ImageWidth / ImageHeight اندازهٔ قاب بر حسب point است
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// صفحه فقط فرم واردشده را فراخوانی میکند: 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 به یک form XObject تبدیل میشود، پس GetPageContentToString فقط توالی save و transform و Do و restore را برمیگرداند. عملگرهای m و l و c و h و S که از رکوردهای Poly* تولید شدهاند داخل استریم form XObject زندگی میکنند که فشرده است. برای ممیزیشان فایل ذخیرهشده را در یک PDF object inspector دیکامپرس کن و استریم فرم را بخوان: برای فایل تست بالا باید ببینی polyline با S تمام میشود بدون هیچ h قبلش، در هر فلگ بستن در شکلهای PolyDraw یک h هست، و روی هیچکدام از این subpathها f یا B نیست. DrawImage همچنین یک EMF واردشده را یکنواخت با کوچکترین Width و Height مقیاس میکند، پس رسم نسبت ابعادش را نگه میدارد حتی اگر جعبهای که میدهی با آن نخواند
برای تارگتهای Free Pascal، ساختن importer برداری EMF در PDFlibPas زیر Free Pascal را ببین؛ معناشناسی رکوردها هر جا که importer کامپایل شود یکسان است
parser EMF باید با شمارندههای نقطهٔ داخل فایل چطور رفتار کند؟
یک parser EMF باید هر شمارندهٔ نقطه را ورودی غیرقابلاعتماد بداند و قبل از کپی کردن حتی یک نقطه آن را با اندازهٔ رکورد بسنجد. EnumEnhMetaFile فقط تضمین میکند nSize هر رکورد داخل فایل بماند. بررسی نمیکند که cptl با nSize سازگار باشد، پس handler ای که cptl نقطه را با Move کپی میکند وقتی شمارنده جعلی یا خراب است رکوردهای بعدی را میخواند، یا از انتهای متافایل رد میشود. از v3.539.39 PDFlibPas برای PolyDraw و PolyBezier و PolyBezierTo و Polyline و PolylineTo و Polygon در هر دو عرض، header ثابت بهعلاوهٔ شمارنده ضربدر بایت بهازای هر نقطه را با nSize میسنجد، با یک بایت اضافه بهازای هر نقطه برای بایتهای نوع PolyDraw. برای رکوردهای PolyPoly شمارندههای هر شکل هم باید جمعشان از مجموع اعلامشده بیشتر نشود، و شکلهای صفرنقطهای رد میشوند
همین چک به اندازهٔ کافی کوتاه است که در parser خودت کپی کنی. این نسخه یک EMR_POLYPOLYLINE 32 بیتی را اعتبارسنجی میکند و اشارهگری به آرایهٔ واقعی نقاطش برمیگرداند:
uses
Winapi.Windows;
// nil برمیگرداند مگر رکورد واقعاً نقاطی را که declare کرده نگه دارد.
// نقاط بعد از آرایهٔ شمارندهها شروع میشوند: 32 + nPolys * 4 بایت داخل،
// نه در aptl[0] که RTL آن را بهشکل آرایهٔ تکعنصری declare میکند
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; // شمارنده جعلی یا بریدهشده
Total := 0;
Count := @P^.aPolyCounts[0]; // پیمایش با اشارهگر: [0..0] range check را میاندازد
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // شکلها نقاط بیشتر از موجود ادعا میکنند
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
تست جا شدن نقاط اول اجرا میشود، پس قبل از اینکه حلقه آرایهٔ شمارندهها را پیمایش کند معلوم است داخل رکورد است. محاسبات Int64 است چون nPolys * 4 و cptl * 8 در 32 بیت میتوانند سرریز کنند و از مقایسه رد شوند
مرجع سریع: قواعد Poly* در EMF برای تبدیل EMF به PDF
EMR_POLYBEZIER: نقطهٔ 0 نقطهٔ شروع است؛ از نقطهٔ 1 به بعد سهتایی گروه کن؛ رکورد 32 بیتی در v3.539.41 فیکس شدEMR_POLYLINE/ EMR_POLYPOLYLINE: شکلهای باز، فقط stroke باS، هرگزhیاfیاB، چون fill در PDF subpathهای باز را میبنددEMR_POLYLINETO: از موقعیت فعلی شروع کن، باز بمان، موقعیت فعلی را بهروز کن، به وضعیت pen دست نزنEMR_POLYPOLYLINE32 بیتی: نقاط از بایت32 + nPolys * 4شروع میشوند، نه ازaptl[0]EMR_POLYDRAW: PT_CLOSEFIGUREرا قبل از dispatch ماسک کن، بعد از سگمنت کامل ببند، وقتی نقطهٔ اولPT_MOVETOنیست از موقعیت فعلی شروع کن- وضعیت پیشفرض device context
BLACK_PENبهعلاوهٔWHITE_BRUSHاست؛ نسخهٔ v3.539.43 و بعد از آن به آن احترام میگذارند - داخل
BeginPath/ EndPath، هر polyline subpath خودش را باز میکند و تا وقتی براکت مصرف نشده هیچچیز نقاشی نمیشود - هر
cptl/ cptsرا قبل از کپی نقاط در محاسبات 64 بیتی باnSizeاعتبارسنجی کن - EMFهای تست دستساز به header کامل 108 بایتی نیاز دارند، وگرنه
TMetafile.LoadFromStreamآنها را WMF میخواند
اگر گزارشهایت از کامپوننت دیگری رد میشوند، همین معناشناسی رکورد صادق است؛ import برداری EMF و WMF در HotPDF توضیح میدهد آن کامپوننت چطور brushهای gradient و hatch را به patternهای PDF تبدیل میکند، و گرافیک برداری و shaderها و gradientها در PDFlibPas کشیدن همان شکلها را مستقیم با API کتابخانه بهجای واسطهٔ متافایل پوشش میدهد
PDFlibPas نسخهٔ v3.539.43 و بعدتر همهٔ قواعد بالا را شامل میشوند. جزئیات و دانلود نسخهٔ آزمایشی در صفحهٔ محصول PDFlibPas Delphi PDF library در دسترس است