مقاله فنی

ایجاد فایل‌های PDF از ابتدا با PDFium Component در دلفی

PDFium به عنوان یک موتور نمایشگر، رندرکننده پشت برگه PDF کروم، شهرت دارد، بنابراین اولین چیزی که باید روشن شود این است که PDFium Component همچنین می‌تواند سندی را بسازد که قبلاً هرگز وجود نداشته است. سمت تألیف (authoring) به دور API شیء-صفحه (page-object) از PDFium می‌پیچد: یک سند خالی می‌سازید، صفحاتی با ابعاد واضح اضافه می‌کنید، و متن، مسیرهای برداری، و تصاویر را در هر صفحه در مختصاتی که انتخاب می‌کنید رها می‌کنید. هیچ زبان توصیف صفحه‌ای برای یادگیری و هیچ درایور چاپی در حلقه وجود ندارد. شما متدها را فراخوانی می‌کنید، کتابخانه اشیاء PDF را مونتاژ می‌کند و SaveAs نتیجه را 직렬‌بندی (serialize) می‌کند

چیزی که به دست نمی‌آورید یک موتور طرح‌بندی (layout) است. این به اندازه‌ای اهمیت دارد که از همان ابتدا گفته شود، زیرا هر مثالی در زیر را شکل می‌دهد. PDFium Component محتوا را در جایی که شما به آن می‌گویید قرار می‌دهد، در مختصات مطلق، و نه هیچ جای دیگر. این یک پاراگراف را جمع نمی‌کند (wrap)، متن را در سراسر یک شکستگی صفحه به جریان نمی‌اندازد، یا یک جدول را از سطرها و ستون‌ها محاسبه نمی‌کند. اینها کار شما هستند. اگر انتظار داشتید چیزی داشته باشید که نثر را به روشی که یک واژه‌پرداز انجام می‌دهد، بازآرایی (reflow) کند، اکنون خود را کالیبره کنید: این یک API مکان‌یابی دقیق و سطح پایین است که بیشتر به نقاشی روی بوم (canvas) نزدیک است تا حروف‌چینی (typesetting) یک سند. برای فاکتورهای تولید شده، گواهی‌ها، برچسب‌ها و صفحات گزارش که قبلاً می‌دانید هر عنصر به کجا تعلق دارد، این دقت دقیقاً همان چیزی است که شما می‌خواهید

حداقلی که یک فایل را تولید می‌کند

سه فراخوانی بین یک TPdf خالی و یک PDF ذخیره شده قرار دارند: سند را ایجاد کنید، یک صفحه اضافه کنید، آن را بنویسید (خروجی بگیرید). هر چیز دیگری محتوایی است که بین آن‌ها لایه‌بندی می‌کنید

uses
  Vcl.Graphics,   // for clBlack and TColor
  PDFium;         // TPdf lives here

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // empty in-memory document
    Pdf.AddPage(0, 595, 842);           // A4 portrait, in points
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialize to disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

یک جزئیات، افرادی را که قطعه کدهای قدیمی‌تر را دیده‌اند، به اشتباه می‌اندازد: شما Pdf.Active := True را بعد از CreateDocument اختصاص نمی‌دهید. ویژگی Active گزارش می‌دهد که آیا دستگیره (handle) سند وجود دارد یا خیر، و CreateDocument از قبل یکی را ایجاد کرده است، بنابراین در لحظه‌ای که این فراخوانی برمی‌گردد، این ویژگی True است. تنظیم مجدد آن در بهترین حالت هیچ عملی انجام نمی‌دهد (no-op) و در بدترین حالت خواننده بعدی را گمراه می‌کند. Active در راه خروج ارزش خود را نشان می‌دهد: تخصیص False سند زیرین را قبل از Free آزاد می‌کند، که نظم تخریب (teardown) تمیزی است. با CreateDocument و بازکردن یک فایل به عنوان موارد متقابلاً منحصر به فرد (mutually exclusive) رفتار کنید. کتابخانه از ایجاد یک سند جدید بر روی TPdf که از قبل باز است خودداری می‌کند، بنابراین استفاده مجدد به این معنی است که ابتدا سند فعلی را ببندید

مختصات از پایین سمت چپ شروع می‌شود

دومین جفت آرگومان برای AddText، و برای هر فراخوانی مکان‌یابی، یک نقطه در فضای کاربر PDF است. مبدأ در گوشه پایین سمت چپ صفحه قرار دارد، X به سمت راست می‌رود، و Y بالا می‌رود. یک واحد یک پوینت، 1/72 اینچ است، بنابراین یک صفحه A4 595 در 842 واحد و US Letter 612 در 792 واحد است. Y رو به بالا شایع‌ترین منبع سردرگمی "متن من خارج از صفحه است" است، زیرا مختصات صفحه‌نمایش و بیت‌مپ مبدأ را در بالا قرار می‌دهند در حالی که Y به سمت پایین رشد می‌کند. در یک صفحه با ارتفاع 842 پوینت، عنوانی در نزدیکی بالای صفحه در حدود Y 780 قرار می‌گیرد، نه Y 60. وقتی یک اجرا در جایی غیرمنتظره قرار می‌گیرد، ارتفاع صفحه منهای Y شما، تقریباً همیشه عددی است که واقعاً منظور شما بوده است

AddPage یک موقعیت درج (insertion position) را به عنوان اولین آرگومان خود در نظر می‌گیرد که مبتنی-بر-یک (one-based) بیان می‌شود و 0 یک میانبر راحت "شروع سند" است. برای صفحه اول 0 یا 1 را پاس دهید و صفحه در جلو درج می‌شود؛ مقداری مطابق با تعدادی که برای افزودن به انتها اضافه می‌کنید، پاس دهید. صفحه جدید اضافه شده نیز به صفحه فعلی تبدیل می‌شود، همان صفحه‌ای که فراخوانی‌های بعدی نقاشی، آن را هدف قرار می‌دهند، بنابراین مرحله "انتخاب این صفحه" به صورت مجزا پس از افزودن آن وجود ندارد. اگر چندین صفحه اضافه کنید و بعداً نیاز به کشیدن (draw) دوباره روی یکی از صفحات قبلی داشته باشید، PageNumber را برای جابجایی مکان‌نما تنظیم کنید؛ در حالی که صفحات را به ترتیبی که آنها را ایجاد می‌کنید پر می‌کنید، می‌توانید آن را به حال خود رها کنید

نوشتن متن و قانون فونت که بی سر و صدا گاز می‌گیرد (گیر می‌اندازد)

امضای AddText همه چیزهایی را که یک اجرای منفرد نیاز دارد در خود جای می‌دهد: رشته، نام فونت، اندازه در نقاط، لنگر (anchor) X و Y، سپس رنگ اختیاری، یک بایت آلفا برای شفافیت (transparency)، و زاویه چرخش برحسب درجه

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Title in black, default opacity, no rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // A lighter byline 24 points below it
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // A faint diagonal draft stamp across the page
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

بایت آلفا از 00$ (نامرئی) تا FF$ (مات) اجرا می‌شود، که همین امر باعث می‌شود مهر پیش‌نویس (draft stamp) به جای یک بلوک جامد، یک واترمارک باشد: 30$ تقریباً نوزده درصد کدورت است، که برای خواندن کافی است. این زاویه باعث چرخش اجرا در خلاف جهت عقربه‌های ساعت به دور لنگر خود می‌شود، بنابراین 45 درجه یک مهر کلاسیک گوشه به گوشه (corner-to-corner) می‌دهد. هیچ‌کدام از اینها به یک ویژگی واترمارک مجزا نیاز ندارند. واترمارک تنها یک فراخوانی بزرگ، نیمه شفاف و چرخشی AddText است، و رسم آن قبل یا بعد از بدنه تعیین می‌کند که در پشت یا بالای محتوا قرار می‌گیرد

فونت‌ها مستحق یک جمله دقیق هستند، زیرا حالت خرابی بی سر و صدا (quiet) است. وقتی نام فونتی را پاس می‌دهید، PDFium Component داده‌های TrueType آن فونت را از سیستم عامل درخواست می‌کند و آن را در سند تعبیه (embed) می‌کند، به همین دلیل است که یک فایل ساخته شده در دستگاه شما به صورت مشابهی روی دستگاهی که هرگز این فونت را نصب نکرده است رندر می‌شود. نکته در این است که وقتی نام حل نمی‌شود (does not resolve) چه اتفاقی می‌افتد: یک اشتباه تایپی، یا چهره‌ای که به سادگی روی دستگاه ساخت (build machine) وجود ندارد. هیچ استثنایی وجود ندارد. کتابخانه به ایجاد یک شیء متنی باز می‌گردد که نام را فقط به عنوان یک برچسب با خود حمل می‌کند، بدون اینکه چیزی تعبیه شود، و بیننده را وا می‌گذارد تا هر چیزی را که نزدیک می‌داند جایگزین کند. متن در تست‌های شما ظاهر می‌شود، معقول (plausible) به نظر می‌رسد، و در لحظه‌ای که فایل در جایی باز می‌شود که فونت‌های متفاوتی نصب شده است، متریک‌ها یا گلیف‌ها را تغییر می‌دهد. از نام‌هایی استفاده کنید که می‌دانید در ماشین تولید کننده وجود دارند، با لیست فونت‌ها به عنوان وابستگی به استقرار رفتار کنید، و قبل از اینکه به خروجی اعتماد کنید، نمونه‌ای را در نمایشگر (viewer) یک سیستم تمیز باز کنید

اشکال برداری: یک مسیر بسازید، سپس آن را متعهد (commit) کنید

خطوط، مستطیل‌ها، و مناطق پرشده از یک مسیر عبور می‌کنند. شما یکی را با CreatePath باز می‌کنید، که نقطه شروع و تمام سبک‌ها (styling) را به‌طور یکجا تنظیم می‌کند، حالت پُر کردن (fill mode)، رنگ‌های پُر کردن (fill) و خط (stroke) با بایت‌های آلفای خودشان، عرض خط، سرپوش‌های خط (line caps) و اتصال‌ها (joins). سپس آن را با LineTo، BezierTo، و ClosePath گسترش می‌دهید، و در نهایت AddPath مسیر تمام شده را در صفحه متعهد (commit) می‌کند. فراموش کردن گام تعهد (commit) آسان است و اگر از آن بگذرید چیزی تولید نمی‌کند

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // A thin horizontal rule. The rectangle overload sets a box directly:
  // X, Y, Width, Height, then fill mode and colors.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Point overload: start at the first vertex, line to the rest, close.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nothing is drawn until this runs
end;

دو افزار (overloads) موارد رایج را پوشش می‌دهند. فرم چهار-مختصاتی (four-coordinate) مقادیر X، Y، عرض (width) و ارتفاع (height) را می‌گیرد و یک مستطیل هم‌راستا با محور در یک فراخوانی به شما می‌دهد که برای کشیدن یک خط‌کش (rule)، حاشیه سلول (cell border) یا پنل پس‌زمینه پرشده به آن دسترسی دارید. فرم دو-مختصاتی (two-coordinate) فقط یک نقطه شروع را تنظیم می‌کند و بقیه طرح را خودتان با LineTo و BezierTo دنبال می‌کنید (trace). حالت پر کردن (Fill mode) نحوه رنگ‌آمیزی مناطق هم‌پوشانی (overlapping regions) را کنترل می‌کند: fmWinding (غیرصفر) مناسب بیشتر اشکال جامد (solid) است، fmAlternate (زوج-فرد) خطوط بُرش‌خورده و خطوط تقاطع-شخصی (self-intersecting) را کنترل می‌کند، و fmNone یک مسیر فقط کشیده‌شده (stroked-only) بدون پر کردن باقی می‌گذارد، که تقسیم‌کننده در بالا از آن استفاده می‌کند

جداول مسیرها و متنی هستند که با دست مونتاژ شده‌اند

از آنجایی که هیچ اولیه (primitive) جدولی وجود ندارد، جدول یک حلقه است. شما در مورد افست‌های X ستون و ارتفاع ردیف تصمیم می‌گیرید، هر سلول را با AddText بنویسید و قوانین (rules) را با مسیرهای مستطیلی ترسیم کنید. این محاسبه مال شماست، اما ساده است و پس از نوشتن، به هر شبکه‌ای که نیاز دارید تعمیم می‌یابد

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // column offsets
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Header row
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Rule under the header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Data rows, stepping Y downward each iteration
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

توجه کنید که در هر عبور Y به اندازه ارتفاع سطر به سمت پایین گام برمی‌دارد، باز هم به این دلیل که سمت بالا مثبت است. در اینجا نیز عدم وجود اندازه‌گیری متن خود را نشان می‌دهد: هیچ چیزی مانع از سرریز (overrun) شدن نام یک مورد طولانی به ستون بعدی نمی‌شود، زیرا کتابخانه نمی‌داند رشته شما چقدر عریض رندر شده است. برای خروجی با قالب ثابت (fixed-format) که در آن داده‌ها را کنترل می‌کنید، ستون‌ها را سخاوتمندانه اندازه‌بندی می‌کنید و ادامه می‌دهید. برای محتوای واقعاً متغیر، یا ورودی‌ها را محدود می‌کنید یا قبل از قرار دادن آنها، عرض‌های گلیف را خودتان اندازه می‌گیرید، که این نقطه‌ای است که یک کتابخانه اختصاصی ترکیب‌بندی شروع به پرداختن هزینه‌های خود می‌کند

تصاویر و چندین صفحه

محتوای شطرنجی (Raster) از طریق کمک‌کننده‌های تصویر وارد می‌شود. AddPicture یک TPicture بارگذاری شده را می‌گیرد و آن را در نقطه‌ای قرار می‌دهد، با عرض و ارتفاع اختیاری برای مقیاس‌بندی آن؛ AddImage به طور مستقیم مسیر فایل یا TBitmap را می‌پذیرد و AddJpegImage بایت‌های JPEG را بدون نیاز به رفت و برگشت از طریق بیت‌مپ استریم (stream) می‌کند. مانند سایر موارد، مختصات مکان‌یابی، گوشه پایین سمت چپ تصویر در فضای کاربر است و عرض و ارتفاع اندازه درون-صفحه به پوینت است، نه ابعاد پیکسلی منبع

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; the new page becomes current
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer near the bottom edge
      // ... draw this page's body here ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

یک سند چند صفحه‌ای، الگوی تک صفحه‌ای در یک حلقه است. هر AddPage یک صفحه را ضمیمه می‌کند و آن را فعلی می‌سازد، بنابراین بدنه و پاورقی که شما رسم می‌کنید روی صفحه‌ای که به تازگی اضافه کرده‌اید فرود می‌آیند (قرار می‌گیرند). در داخل این حلقه، PageNumber را مجدداً اختصاص نمی‌دهید، زیرا افزودن یک صفحه از قبل مکان‌نما را به آنجا منتقل کرده است؛ فقط وقتی به صفحه‌ای خارج از ترتیب ایجاد بازمی‌گردید به PageNumber نیاز دارید. در پایان یک‌بار، پس از پر شدن صفحه آخر، SaveAs را فراخوانی کنید. اگر به جای یک فایل ساده به یک نمایه آرشیوی نیاز دارید، همان شیء سند SaveAsPdfA و سایر انواع انطباق را در معرض دید قرار می‌دهد، بنابراین انتخاب استاندارد خروجی یک فراخوانیِ ذخیرهِ متفاوت است، نه یک مسیر ساخت متفاوت

کجا مناسب است

کادربندی (framing) صادقانه این است که API تألیفِ (authoring API) PDFium Component یک لایه وفادار و نازک روی مدل شیء-صفحه (page-object) از PDFium است: ایجاد سند واقعی، فونت‌های تعبیه‌شده واقعی، محتوای برداری و شطرنجی (raster) واقعی، که به یک فایل مطابق با استانداردها 직렬‌بندی (serialized) می‌شود. این یک موتور سند با بازآرایی (reflowing) نیست و وانمود نمی‌کند که باشد. خط تقسیم (dividing line)، طرح‌بندی (layout) متن است. اگر خروجی شما الگو-بندی (templated) شده است، فاکتورها، گواهی‌ها، برچسب‌ها، داشبوردهایی که به یک شبکه (grid) ثابت رندر می‌شوند، مدل مختصات-مطلق مستقیم و سریع است و کد خوانا باقی می‌ماند. اگر خروجی شما نثر طولانی‌مدتی است که باید خود را جمع‌بندی (wrap) و صفحه‌بندی کند، موتور طرح‌بندی (layout) را روی این فراخوانی‌ها بازسازی (rebuild) خواهید کرد، و این ابزار اشتباهی برای کار است. دانستن اینکه در کدام سمت آن خط قرار دارید، بیشترین بخشِ تصمیم‌گیری است

متدهای ایجاد توضیح داده شده در اینجا بخشی از PDFium Component برای دلفی هستند، که این مسیر تألیف را با ویژگی‌های رندرینگ و استخراج متن که PDFium با آن‌ها شناخته شده‌تر است جفت می‌کند