Техническа статия

Създаване на PDF файлове от нулата с PDFium Component в Delphi

PDFium има репутация на енджин за преглед, рендерерът зад раздела за PDF в Chrome, така че първото нещо, което трябва да се изясни, е, че PDFium Component може също да изгради документ, който никога преди не е съществувал. Страната на създаването обвива (wraps) API-то за обекти на страници на PDFium: правите празен документ, добавяте страници с изрични размери и пускате текст, векторни пътища и изображения върху всяка страница на координати, които изберете. Няма език за описание на страници за учене и няма драйвер за печат в цикъла. Викате методи, библиотеката сглобява PDF обекти, а SaveAs сериализира резултата

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

Минимумът, който произвежда файл

Три извиквания стоят между празен TPdf и запазен PDF: създайте документа, добавете страница, запишете го. Всичко останало е съдържание, което наслоявате между тях

uses
  Vcl.Graphics,   // за clBlack и TColor
  PDFium;         // TPdf живее тук

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // празен документ в паметта
    Pdf.AddPage(0, 595, 842);           // A4 портрет, в точки
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // сериализиране на диск
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Една подробност препъва хората, които са виждали по-стари фрагменти: не присвоявате Pdf.Active := True след CreateDocument. Свойството Active отчита дали съществува манипулатор на документ (document handle), а CreateDocument вече е създал такъв, така че свойството е True в момента, в който това извикване се върне. Задаването му отново е в най-добрия случай липса на операция (no-op), а в най-лошия – подвеждащо за следващия читател. Active изкарва прехраната си на излизане: присвояването на False освобождава лежащия отдолу документ преди Free, което е чистият ред на разглобяване. Третирайте CreateDocument и отваряне за зареждане на файл като взаимно изключващи се. Библиотеката отказва да създаде нов документ на TPdf, който вече има отворен такъв, така че повторното използване означава първо затваряне на текущия документ

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

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

AddPage приема позиция за вмъкване като свой първи аргумент, изразена базирано на едно (one-based), като 0 е удобно съкращение за „начало на документа“. Подайте 0 или 1 за първата страница и страницата се вмъква отпред; подайте стойността, съответстваща на броя, към който добавяте (appending), за да добавите в края. Новодобавената страница също така става текущата страница, тази, към която са насочени последващите извиквания за рисуване, така че няма отделна стъпка „избери тази страница“ след добавянето й. Ако добавите няколко страници и по-късно трябва да рисувате обратно върху по-ранна такава, задайте PageNumber, за да преместите курсора; докато попълвате страници по ред, докато ги създавате, можете да го оставите на мира

Писане на текст и правилото за шрифтовете, което хапе мълчаливо

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

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Заглавие в черно, непрозрачност по подразбиране, без ротация
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // По-светъл ред с автор (byline) на 24 точки под него
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // Блед диагонален печат за чернова през страницата
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Алфа байтът върви от $00 (невидим) до $FF (непрозрачен), което е това, което прави печата за чернова воден знак, а не плътен блок: $30 е приблизително деветнадесет процента непрозрачност, достатъчно за четене през него. Ъгълът завърта текста обратно на часовниковата стрелка около неговата котва, така че 45 градуса дава класическия печат от ъгъл до ъгъл. Нищо от това не се нуждае от отделна функция за воден знак. Водният знак е просто голямо, полупрозрачно, завъртяно извикване на AddText, и рисуването му преди или след тялото решава дали ще седи зад или върху съдържанието

Шрифтовете заслужават внимателно изречение, защото режимът на отказ е тих. Когато подадете име на шрифт, PDFium Component иска от операционната система данните за TrueType на този шрифт и ги вгражда в документа, поради което файл, изграден на вашата машина, се рендира идентично на такава, на която шрифтът никога не е бил инсталиран. Уловката е какво се случва, когато името не се разрешава: печатна грешка или начертание (face), което просто не присъства на машината за изграждане. Няма изключение. Библиотеката преминава към създаване на текстов обект, който носи името само като етикет, без нищо вградено, и оставя на програмата за преглед да замести каквото сметне за близко. Текстът се появява във вашите тестове, изглежда правдоподобно и измества метрики или глифове в момента, в който файлът се отвори някъде с инсталирани различни шрифтове. Използвайте имена, за които знаете, че присъстват на генериращата машина, третирайте списъка с шрифтове като зависимост за внедряване и отворете мостра в програма за преглед на чиста система, преди да се доверите на изхода

Векторни форми: изградете път, след което го ангажирайте

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

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // Тънка хоризонтална линия. Претоварването за правоъгълник задава кутия директно:
  // X, Y, Width, Height, след това режим на запълване и цветове.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Претоварване за точка: започнете от първия връх, линия до останалите, затваряне.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // нищо не се рисува, докато това не се изпълни
end;

Две претоварвания покриват често срещаните случаи. Формата с четири координати приема X, Y, ширина и височина и ви дава подравнен по оста правоъгълник в едно извикване, което е това, към което посягате, за да нарисувате линия, граница на клетка или запълнен фонов панел. Формата с две координати задава само начална точка, а вие проследявате останалата част от контура сами с LineTo и BezierTo. Режимът на запълване контролира как се рисуват припокриващи се региони: fmWinding (nenulevo навиване) е подходящо за повечето плътни форми, fmAlternate (четно-нечетно) се справя с изрези и самопресичащи се контури, а fmNone оставя път само с контур без запълване, което е това, което използва разделителят по-горе

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

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

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // отмествания на колони
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Заглавен ред
  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);

  // Линия под заглавието
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Редове с данни, стъпвайки Y надолу при всяка итерация
  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 предава (streams) JPEG байтове без двупосочно пътуване през растерна графика. Както при всичко останало, координатите за разполагане са долният ляв ъгъл на изображението в потребителското пространство, а ширината и височината са размерът на страницата в точки, а не размерите в пиксели на източника

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);     // добавяне; новата страница става текуща
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // долен колонтитул (footer) близо до долния ръб
      // ... нарисувайте тялото на тази страница тук ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Многостраничният документ е моделът с една страница в цикъл. Всяко AddPage добавя страница и я прави текуща, така че тялото и долният колонтитул, които рисувате след това, се приземяват на страницата, която току-що сте добавили. Не преназначавате PageNumber вътре в този цикъл, защото добавянето на страница вече е преместило курсора там; нуждаете се от PageNumber само когато се върнете към страница извън реда на създаване. Извикайте SaveAs веднъж в края, след като последната страница е попълнена. Ако имате нужда от архивен профил, а не от обикновен файл, същият обект на документа излага SaveAsPdfA и другите варианти за съответствие (conformance variants), така че изборът на стандарт за изход е различно извикване за запазване, а не различен път на изграждане

Къде се вписва това

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

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