Technical Article

Създаване на PDF документи от нулата с PDFium VCL в Delphi

PDFium е известен най-вече като двигател за разглеждане на документи â€?рендериращото ядро зад PDF раздела в Chrome. Затова първото нещо, което трябва да уточним, е че PDFium VCL може също така да изгражда съвсем нови документи от нулата. Страната на писане и редактиране обвива API за обекти на страници на PDFium: създавате празен документ, добавяте страници с точни размери и поставяте текст, векторни пътища и изображения на всяка страница по избрани от вас координати. Няма език за описание на страници, който да учите, нито печатащ драйвер по веригата. Вие просто извиквате методи, библиотеката сгловява PDF обектите, а методът SaveAs сериализира резултата.

Това, което няма да получите обаче, е двигател за автоматично оформление (layout engine). Това е важно да се каже предварително, тъй като определя начина на работа във всеки пример по-долу. PDFium VCL поставя съдържанието точно там, където му укажете â€?в абсолютни координати и никъде другаде. Той няма да пренесе автоматично абзац на нов ред, няма да прехвърли текст при прекъсване на страницата и няма да изчисли таблица въз основа на редове и колони. Тези задачи са ваша отговорност. Ако очаквате нещо, което пренарежда текста по начина, по който го прави текстообработваща програма, е важно да коригирате очакванията си: това е прецизен API за ниско ниво на позициониране, по-близък до изчертаване върху платно (canvas), отколкото до текстонабор на документ. За генериране на фактури, сертификати, етикети и страници с отчети, където предварително знаете точното местоположение на всеки елемент, тази прецизност е точно това, което търсите.

Минимумът, необходим за създаване на файл

Само три извиквания стоят между празния обект 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 показва дали съществува дескриптор на документ (document handle), а CreateDocument вече е създал такъв, така че свойството е True в момента, в който това извикване приключи. Повторното му задаване в най-добрия случай не прави нищо, а в най-лошия подвежда следващия разработчик, който чете кода. Свойството Active е полезно при освобождаване на ресурсите: присвояването на False освобождава документа преди извикването на Free, което е правилната последователност на демонтиране. Третирайте CreateDocument и отварянето за зареждане на файл като взаимно изключващи се операции. Библиотеката отказва да създаде нов документ върху обект TPdf, който вече има отворен такъв, така че повторната употреба изисква първо да затворите текущия документ.

Координатите започват от долния ляв ъгъл

Втората двойка аргументи на AddText, както и при всяко извикване за позициониране, представлява точка в потребителското пространство на PDF. Координатното начало е в долния ляв ъгъл на страницата, X нараства надясно, а Y нараства нагоре. Една мерна единица е една точка (point) â€?1/72 от инча, така че страница с формат A4 е 595 на 842 единици, а US Letter е 612 на 792. Това нарастване на Y нагоре е най-честата причина за недоразумения от типа „текстъÑ?ми е извън страницатаâ€? тъй като екранните координати и тези при растерните изображения поставят началото в горния край с Y, нарастващ надолу. На страница с височина 842 точки заглавие близо до горния ръб се намира на Y 780, а не на Y 60. Когато даден елемент се озове на неочаквано място, височината на страницата минус стойността на Y почти винаги е числото, което всъщност сте искали да зададете.

Методът AddPage приема позиция за вмъкване като първи аргумент, започваща от 1, като 0 е удобно съкращение за „началÐ?на документаâ€? Предайте 0 или 1 за първата страница и тя ще бъде вмъкната най-отпред; предайте стойност, съответстваща на броя страници, за да я добавите в края. Новосъздадената страница става и текуща страница â€?тази, към която са насочени следващите извиквания за изчертаване, така че няма отделна стъпка за „избор на тази страницаâ€?след добавянето й. Ако добавяте няколко страници и по-късно се наложи да рисувате върху някоя от по-ранните, задайте стойност на PageNumber, за да преместите курсора; докато попълвате страниците последователно при създаването им, можете да не променяте това свойство.

Писане на текст и правилото за шрифтовете, което може да ви подведе

Сигнатурата на AddText съдържа всичко необходимо за един пасаж от текст: низ, име на шрифт, размер в точки, координатите X и Y за закотвяне, последвани от незадължителен цвят, алфа байт за прозрачност и ъгъл на ротация в градуси.

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 (непрозрачен), което прави надписа за чернова воден знак, а не плътен блок: стойността $30 осигурява около деветнадесет процента непрозрачност, което е достатъчно, за да се вижда текстът отдолу. Ъгълът завърта текста обратно на часовниковата стрелка около точката на закотвяне, така че 45 градуса дават класическия диагонален надпис. Нищо от това не изисква специална функция за воден знак. Водният знак е просто голямо, полупрозрачно и завъртяно извикване на AddText, а изчертаването му преди или след основното тяло определя дали той ще стои зад или пред съдържанието.

Шрифтовете заслужават специално внимание, тъй като грешките при тях са тихи. Когато подадете име на шрифт, PDFium VCL изисква TrueType данните за този шрифт от операционната система и ги вгражда в документа. Ето защо файл, създаден на вашия компютър, се визуализира по абсолютно същия начин на система, на която шрифтът никога не е бил инсталиран. Проблемът възниква, когато името не може да бъде разпознато: поради печатна грешка или ако шрифтът просто липсва на машината, на която се компилира проектът. При това не възниква изключение. Библиотеката се връща към създаване на текстов обект, който носи името само като етикет без вграждане на данни, оставяйки четеца да направи замяна с шрифт, който смята за най-близък. Текстът се появява при вашите тестове, изглежда нормално, но променя метриките или глифовете в момента, в който файлът се отвори на система с други инсталирани шрифтове. Използвайте имена, за които сте сигурни, че присъстват на генериращата машина, третирайте списъка с шрифтове като зависимост при внедряване и винаги отваряйте примерен файл в четец на чиста система, преди да се доверите на резултата.

Векторни форми: създаване на път и потвърждаването му

Линиите, правоъгълниците и запълнените области се създават чрез път (path). Започвате път с CreatePath, който задава началната точка и всички стилове наведнъж â€?режим на запълване, цветове на запълване и контур със съответните им алфа байтове, ширина на контура, завършвания на линиите (line caps) и съединявания (joins). След това го разширявате с LineTo, BezierTo и ClosePath, и накрая AddPath записва завършения път върху страницата. Тази стъпка на записване лесно се забравя, а без нея на екрана няма да се изобрази нищо.

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) покриват най-честите случаи. Версията с четири координати приема X, Y, ширина и височина и ви дава подравнен правоъгълник с едно извикване â€?това, което ви е необходимо за изчертаване на линия, рамка на клетка или запълнен фонов панел. Версията с две координати задава само начална точка, а останалата част от контура очертавате сами с LineTo и BezierTo. Режимът на запълване (fill mode) определя как се оцветяват застъпващите се области: fmWinding (nonzero winding) е подходящ за повечето плътни форми, fmAlternate (even-odd) управлява изрязванията и самопресичащите се контури, а fmNone оставя само очертан път без запълване, което се използва за разделителната линия по-горе.

Таблиците се състоят от пътища и текст, сглобени ръчно

Тъй като няма готов елемент за таблица, таблицата се изгражда чрез цикъл. Вие определяте отместванията по координатата X за колоните и височината на реда, изписвате всяка клетка с AddText и изчертавате линиите с правоъгълни пътища. Аритметиката е ваша задача, но е лесна и веднъж написана, може да се приложи за всяка мрежа (grid), от която се нуждаете.

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 се движи надолу с височината на реда при всяка стъпка, тъй като посоката нагоре е положителна. Тук липсата на измерване на дължината на текста става видима: нищо не пречи на дълго име на артикул да навлезе в съседната колона, тъй като библиотеката не знае колко широк е рендираният низ. За документи с фиксиран формат, при които контролирате данните, можете просто да зададете достатъчно широки колони. За наистина динамично съдържание трябва или да ограничите входните данни, или сами да измерите ширината на глифовете, преди да ги разположите â€?момент, при който специализирана библиотека за оформление започва да си възвръща инвестицията.

Изображения и множество страници

Растерното съдържание се въвежда чрез помощни методи за изображения. AddPicture приема зареден обект TPicture и го разполага в дадена точка с незадължителни размери за мащабиране; AddImage приема директно път към файл или обект TBitmap, а AddJpegImage предава компресираните JPEG байтове, без да преминава през bitmap. Както при всичко останало, координатите за позициониране са долният ляв ъгъл на изображението в потребителското пространство, а ширината и височината са размерите върху страницата в точки, а не размерите в пиксели на изходния файл.

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 добавя страница и я прави текуща, така че основното тяло и долният колонтитул (footer), които изчертавате след това, се позиционират върху страницата, която току-що сте добавили. Не е необходимо да променяте PageNumber в този цикъл, тъй като добавянето на страница автоматично премества курсора там; свойството PageNumber ви трябва само ако се връщате към страница извън реда на нейното създаване. Извикайте SaveAs веднъж накрая, след като последната страница е попълнена. Ако имате нужда от архивен профил (PDF/A), а не от обикновен файл, същият обект на документа предлага метода SaveAsPdfA и други съвместими варианти, така че изборът на изходен стандарт е просто различно извикване на метод за запис, а не промяна в логиката на изграждане.

Къде е приложението на този подход

Ако трябва да сме напълно коректни, приложният програмен интерфейс (API) за създаване на документи в PDFium VCL е тънък и надежден слой над модела от обекти на страници на PDFium: реално създаване на документи, вградени шрифтове, векторно и растерно съдържание, записани в съответствие със стандартите. Той не е и не претендира да бъде двигател за автоматично преструктуриране на документи. Разделителната линия е разположението на текста. Ако вашият резултат е базиран на шаблони (фактури, сертификати, етикети, табла с данни, разположени във фиксирана мрежа), моделът с абсолютни координати е директен, бърз и кодът остава лесен за четене. Ако резултатът ви е дълъг текст, който трябва да се пренася и разпределя на страници автоматично, ще се наложи да изградите свой двигател за оформление над тези извиквания, което прави инструмента неподходящ за целта. Разбирането от коя страна на тази линия се намирате е най-важната част от решението.

Методите за създаване на документи, описани тук, са част от компонента PDFium VCL Component за Delphi, който съчетава възможностите за генериране на документи с функциите за визуализация и извличане на текст, с които PDFium е по-известен.