مقاله فنی

گرافیک برداری در PDF با دلفی: مسیرها و گرادیان‌ها

بیشتر کدهای دلفی که با PDF کار می‌کنند، این فرمت را به عنوان ظرفی برای دو چیز در نظر می‌گیرند: رشته‌هایی از متن و چند بیت‌مپ قرار داده شده. این دیدگاه تا حدی درست است، اما تواناترین بخش فرمت را بلااستفاده می‌گذارد. یک صفحه PDF یک بوم ۲ بعدی مستقل از رزولوشن است که بر روی همان مدل تصویرسازی PostScript ساخته شده است. این بوم می‌تواند خطوط، منحنی‌ها، مناطق پرشده، گرادیان‌ها و الگوهای تکراری را رسم کند، که همه به صورت بردارهایی هستند که در هر زومی واضح باقی می‌مانند و با وضوح کامل دستگاه چاپ می‌شوند. اگر در حال رسم یک لوگو، نمودار، واترمارک یا حاشیه یک گواهینامه هستید، مسیر برداری تقریباً همیشه شیء ابتدایی مناسب است، و این مسیر نسبت به تصویر رستری شده که بسیاری از برنامه‌ها به جای آن استفاده می‌کنند، کوچک‌تر و واضح‌تر است

این مقاله مدل برداری را همانطور که ISO 32000-1 تعریف می‌کند مرور می‌کند و فراخوانی‌های مربوطه در PDFlibPas را نشان می‌دهد. هدف این است که مشخصات ملموس شود، زیرا API به دقت با آن تطبیق دارد، و درک یکی به شما دیگری را می‌آموزد

صفحه یک ماشین مسیر است

استاندارد ISO 32000-1 §8.5 گرافیک را در دو مرحله توصیف می‌کند که هرگز همپوشانی ندارند. ابتدا شما یک مسیر می‌سازید، که هندسه خالصی است و هیچ نتیجه بصری ندارد. سپس این مسیر را در یک عملیات واحد رنگ‌آمیزی می‌کنید که خطوط پیرامون آن را می‌کشد (stroke)، داخل آن را پر می‌کند (fill)، یا هر دو را انجام می‌دهد. هیچ چیزی در طول ساخت و ساز روی صفحه ظاهر نمی‌شود. مسیر یک دنباله انتزاعی از نقاط و بخش‌هایی است که در حالت گرافیکی نگهداری می‌شود تا زمانی که یک عملگر رنگ‌آمیزی آن را مصرف کند، که در این نقطه مسیر رندر شده و کنار گذاشته می‌شود

یک مسیر از یک یا چند زیرمسیر ساخته شده است. یک زیرمسیر در یک نقطه شروع می‌شود و با اضافه کردن بخش‌ها رشد می‌کند: خطوط مستقیم، منحنی‌های بزیه مکعبی، و در برخی پلتفرم‌ها مستطیل‌های کامل که به عنوان زیرمسیرهای بسته جداگانه اضافه می‌شوند. در PDFlibPas شما یک مسیر را با StartPath باز می‌کنید، که نقطه شروع را تعیین می‌کند، سپس آن را با AddLineToPath و AddCurveToPath گسترش می‌دهید. هر فراخوانی یک نقطه فعلی ضمنی را به جلو می‌برد، بنابراین بخش بعدی از جایی که بخش قبلی پایان یافت ادامه می‌یابد. ClosePath یک بخش مستقیم نهایی را به نقطه شروع زیرمسیر رسم می‌کند، که برای stroke زدن مهم است زیرا به جای دو کلاهک انتهایی شل، یک اتصال خط واقعی در رأس بسته تولید می‌کند

// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // straight segment back to (150, 100)
PDF.DrawPath(2);                   // 2 = fill and stroke; path is consumed

منحنی‌ها از AddCurveToPath استفاده می‌کنند، که دو نقطه کنترل بزیه و یک نقطه پایانی می‌گیرد: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). منحنی از نقطه فعلی به (EndX, EndY) کشیده می‌شود و در طول مسیر به سمت دو نقطه کنترل کشیده می‌شود. کمان‌های دایره‌ای از طریق AddArcToPath(CenterX, CenterY, TotalAngle) در دسترس هستند، که در آن شعاع از فاصله بین نقطه فعلی و مرکز گرفته می‌شود، و موتور، کمان را به صورت زنجیره‌ای از بخش‌های بزیه ساطع می‌کند. مستطیل‌ها یک میانبر دارند، AddBoxToPath(Left, Top, Width, Height)، که یک مستطیل کامل و بسته را به عنوان زیرمسیر خودش بدون نیاز به StartPath قبلی اضافه می‌کند

دو قانون پر کردن (fill rules) و دلیل تفاوت آنها

هنگامی که شما مسیری را پر می‌کنید که از خودش عبور می‌کند یا حاوی یک حلقه داخلی است، رندرکننده برای تصمیم‌گیری اینکه کدام مناطق داخل شکل و کدام سوراخ هستند، نیاز به یک قانون دارد. استاندارد ISO 32000-1 §8.5.3.3 دو قانون را تعریف می‌کند، و آنها می‌توانند هندسه مشابه را به طور متفاوتی رنگ کنند. قانون nonzero winding (پیچش غیر صفر)، تقاطع‌های علامت‌دارِ یک پرتو که از نقطه تست تا بی‌نهایت تابیده می‌شود را می‌شمارد، برای هر بخشی که از چپ به راست عبور می‌کند یک واحد اضافه می‌کند و برای هر بخشی که از مسیر مخالف عبور می‌کند یک واحد کم می‌کند؛ زمانی که مجموع صفر نباشد، نقطه در داخل است. قانون even-odd (زوج-فرد) جهت را نادیده می‌گیرد و به سادگی تقاطع‌ها را می‌شمارد و در صورت فرد بودن شمارش، نقطه را داخل در نظر می‌گیرد

مورد کلاسیکی که در آن این دو با هم متفاوت هستند، شکلی با یک سوراخ، یک دونات یا واشر است. یک مرز خارجی و یک مرز داخلی را داخل آن رسم کنید. بر اساس قانون even-odd، حلقه داخلی همیشه یک سوراخ ایجاد می‌کند، زیرا هر نقطه‌ای بین دو مرز یک بار عبور می‌کند و هر نقطه‌ای در داخل حلقه داخلی دو بار عبور می‌کند. طبق قانون nonzero winding، سوراخ تنها زمانی ظاهر می‌شود که حلقه داخلی در جهت مخالف حلقه بیرونی بپیچد؛ اگر آنها را در یک جهت بپیچانید، پیچش‌ها به جای خنثی شدن تقویت می‌شوند و منطقه داخلی به طور یکپارچه پر می‌شود. یک ستاره پنج پر که به صورت یک خط پیرامونی خود متقاطع رسم می‌شود شکاف مشابهی را نشان می‌دهد: even-odd پنج ضلعی مرکزی را خالی می‌گذارد در حالی که nonzero winding آن را پر می‌کند

در PDFlibPas شما قانون را با فراخوانیِ نقاشی که انجام می‌دهید انتخاب می‌کنید، نه با یک پرچم (flag). متد DrawPath با قانون nonzero winding پر می‌کند؛ DrawPathEvenOdd با قانون even-odd پر می‌کند. هر دو یک حالت صحیح مشابه دریافت می‌کنند: 0 فقط دور خط پیرامونی را می‌کشد (stroke)، 1 فقط داخل را پر می‌کند (fill)، و 2 هم پر می‌کند و هم خط می‌کشد. قانون even-odd دقیقاً به این دلیل ابزار ساده‌تری برای سوراخ‌های پانچ شده است که نیازی به مدیریت جهت زیرمسیرها ندارد

// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120);   // outer
PDF.AddBoxToPath(140, 130, 120,  60);   // inner
PDF.DrawPath(1);                         // 1 = fill, nonzero winding

// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120);   // outer
PDF.AddBoxToPath(140, 330, 120,  60);   // inner cut-out
PDF.DrawPathEvenOdd(1);                  // 1 = fill, even-odd

گرادیان‌های محوری رنگ را در طول یک خط تغییر می‌دهند

رنگ پرکننده یکپارچه (flat fill color) یک مقدار است که در سراسر منطقه قرار می‌گیرد. یک گرادیان رنگ را به طور پیوسته تغییر می‌دهد، و ساده‌ترین نوع آن گرادیان محوری یا خطی است. استاندارد ISO 32000-1 §8.7.4.5 آن را به عنوان یک سایه‌زنی محوری Type 2 (Type 2 axial shading) مشخص می‌کند: شما دو نقطه ارائه می‌دهید که یک محور را تعریف می‌کنند، یک رنگ شروع در نقطه اول و یک رنگ پایان در نقطه دوم، و رندرکننده رنگ را در طول آن محور درون‌یابی (interpolate) می‌کند. هر نقطه‌ای در منطقه پر شده رنگ تصویر عمودی خود را روی محور می‌گیرد، بنابراین گرادیان به صورت نوارهایی با زاویه راست نسبت به خط بین دو نقطه اجرا می‌شود

در PDFlibPas، گرادیان یک منبع سند نام‌گذاری شده است که شما یک بار آن را ایجاد می‌کنید و سپس به عنوان رنگ فعال انتخاب می‌کنید. NewRGBAxialShader آن را ثبت می‌کند. امضای این متد به این شکل است: NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): دو نقطه انتهایی محور، سه‌تایی‌های RGB در هر انتها به عنوان مقادیری در محدوده 0 تا 1، و یک پرچم Extend. در صورت تنظیم Extend روی 1 رنگ‌های انتهایی به صورت رنگ یکپارچه فراتر از نقاط پایانی محور ادامه می‌یابند، که این معمولاً همان چیزی است که می‌خواهید تا گوشه‌های منطقه‌ای در خارج از محور رنگ‌نشده باقی نماند؛ 0 آنها را دست نخورده باقی می‌گذارد. هنگامی که شیدر (shader) وجود دارد، شما آن را با SetFillShader برای مناطق پرشده، SetLineShader برای خطوط پیرامونی یا SetTextShader برای متن پیوند می‌زنید. این اتصال برای فراخوانی‌های رسم بعدی فعال می‌ماند، بنابراین مسیری که در مرحله بعد رنگ‌آمیزی می‌کنید، به جای یک رنگ یکپارچه، گرادیان را دریافت می‌کند

// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // start point and start RGB
  0, 260,   1.00, 1.00, 1.00,    // end point and end RGB
  1);                            // 1 = extend ends as solid color

// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill, now filled by the shader

محور در اینجا عمودی است، از y=100 تا y=260 در یک x ثابت، بنابراین نوارهای رنگ به صورت افقی قرار می‌گیرند و مستطیل از رنگ آبی در پایه به سفید در بالای خود محو می‌شود. از آنجا که شیدر بر اساس نام مشخص می‌شود، یک تعریف می‌تواند به هر تعداد شکل در صفحه رنگ دهد، و بازگشت به یک رنگ یکپارچه فقط با یک فراخوانی دیگرِ SetFillColor قبل از مسیر بعدی انجام می‌شود

الگوهای کاشی‌کاری یک سلول را تکرار می‌کنند

در جایی که یک گرادیان یک رنگ واحد را به صورت هموار تغییر می‌دهد، یک الگوی کاشی‌کاری قطعه کوچکی از کار هنری را در یک منطقه تکرار می‌کند. استاندارد ISO 32000-1 §8.7.3.1 الگوی کاشی‌کاری را به عنوان یک سلول الگو (یک قطعه محتوای مستقل) تعریف می‌کند که رندرکننده آن را روی یک شبکه ثابت تکثیر می‌کند تا منطقه‌ای را که رنگ‌آمیزی می‌شود کاشی‌کاری کند. اینگونه است که شما هاشورزنی برای یک شکل پر شده مهندسی، یک موتیف نام تجاری تکرار شونده در پشت یک هدر، یا یک پس‌زمینه بافت‌دار که بدون توجه به بزرگی ناحیه، کاملاً واضح و برداری می‌ماند و حجم آن تقریباً صفر است را می‌سازید، زیرا سلول یک بار ذخیره می‌شود و همه جا ارجاع داده می‌شود

در PDFlibPas، سلول الگو از محتوای ضبط شده صفحه ساخته می‌شود. شما یک صفحه یا ناحیه را با CapturePage ضبط می‌کنید، این فایل ضبط شده را با NewTilingPatternFromCapturedPage(PatternName, CaptureID) به یک الگوی نام‌گذاری شده تبدیل می‌کنید، و سپس آن الگو را با SetFillTilingPattern(PatternName) به عنوان پُرکننده (fill) فعلی انتخاب می‌کنید. از آن نقطه به بعد، هر مسیری که پُر می‌کنید با سلول تکرارشونده رنگ می‌شود، نه با یک رنگ ساده؛ این دقیقاً مانند پر کردن سایه‌زن کار می‌کند اما با یک سلول کاشی‌کاری شده به عنوان منبع رنگ. این توالی کمی پیچیده‌تر از یک فراخوانی واحد است، بنابراین اگر مرحله ضبط ناآشناست، الگو را به عنوان یک عملیات دو مرحله‌ای در نظر بگیرید: ابتدا سلول ضبط شده را تولید کنید، سپس قبل از کشیدن ناحیه‌ای که می‌خواهید کاشی‌کاری شود، آن را به عنوان یک پرکننده از طریق نام متصل کنید

قرار دادن اشکال اولیه در کنار هم

قطعات به طور مستقیم ترکیب می‌شوند. یک لکه بزیه پرشده مسیری از منحنی‌هاست که با DrawPath رنگ‌آمیزی می‌شود. همان طرح پس از اضافه کردن یک حلقه داخلی و نقاشی با DrawPathEvenOdd سوراخی را نشان می‌دهد که پرکردنِ nonzero آن را بسته بود. یک مستطیل پر از گرادیان یک باکس است که به یک شیدر متصل شده است. مثال زیر هر سه مورد را به ترتیب رسم می‌کند تا تفاوت بین دو قانون پر کردن (fill rules) در یک صفحه قابل مشاهده باشد، سپس یک پنل گرادیان را در زیر آنها قرار می‌دهد

// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480);   // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480);   // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1);                                     // 1 = fill

// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300);                              // new subpath: the hole
PDF.AddArcToPath(200, 300, 360);                     // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1);                              // hole is punched out

// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
  60, 100,  0.95, 0.55, 0.10,
  60, 200,  0.20, 0.10, 0.40,
  1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);

دو نکته ارزش به خاطر سپردن دارند. فراخوانیِ نقاشی، قانون پر کردن (fill rule) را تعیین می‌کند، بنابراین انتخاب بین DrawPath و DrawPathEvenOdd انتخابی بین nonzero winding و even-odd است، و برای اشکال دارای سوراخ، قانون even-odd شما را از استدلال در مورد جهت زیرمسیر نجات می‌دهد. و وضعیت گرافیکی (graphics state) در لحظه‌ای که شما رنگ می‌زنید نمونه‌برداری می‌شود: رنگ‌ها، عرض خط، و پیوند شیدر خود را قبل از فراخوانی نقاشی تنظیم کنید، زیرا این وضعیتی است که موتور می‌خواند. ابتدا بسازید، وضعیت گرافیکی را پیکربندی کنید، و در نهایت رنگ کنید، و مدل برداری هر بار به طور قابل پیش‌بینی رفتار می‌کند

از اینجا، گام‌های طبیعی بعدی شامل خواندن بردارها و متن خارج از یک سند موجود است، که در مقاله ما در مورد استخراج متن، تصویر و فونت پوشش داده شده است، و رندر کردنِ همان مدل نقاشی در زمینه دستگاهِ (device context) ویندوز برای پیش‌نمایش و چاپ روی صفحه است که در راهنمای قدم به قدم چاپ و پیش‌نمایش توضیح داده شده است. فراخوانی‌های مسیر، شیدر و الگو که در اینجا شرح داده شده‌اند، به عنوان بخشی از کتابخانه PDF دلفی در کنار API‌های متن، تصویر، فرم و امضا که در جای دیگرِ این وبلاگ پوشش داده شده‌اند، عرضه می‌شوند