Technical Article

PDF отчети в Delphi с HotPDF: TextOut, шрифтове, изображения

Генерирането на отчет се свежда до поставянето на три неща върху страницата и уеднаквяването на тяхното местоположение: текст на известни координати, шрифтове, които се визуализира по еднакъв начин на сървъра и на вашия компютър, и изображения с подходящ размер. Всичко останало, което прави една библиотека за отчети, е организирано около тези три елемента. HotPDF, библиотеката на losLab за генериране на PDF файлове в Delphi и C++Builder, ви предоставя всяка от тези възможности чрез директно извикване на обекта на страницата. Единственото реално затруднение е базовата координатна система, която работи в посока, обратна на познатото VCL платно (VCL canvas). Уточнете тази ориентация в самото начало и останалата част от работата по оформлението ще спре да ви затруднява.

Позициониране на текст и начало на координатната система в долния ляв ъгъл

Първият отчет на почти всеки разработчик се оказва обърнат наопаки. Заглавието се разполага близо до долния край на страницата, а всеки следващ ред се изкачва нагоре. Системата работи нормално: потребителското пространство на PDF (PDF user space), дефинирано в стандарта ISO 32000-1 §8.3, поставя началото на координатната система в долния ляв ъгъл, като Y расте нагоре. Това е огледален образ на GDI платното, където Y расте надолу от горния ляв ъгъл. Пет минути, отделени за възприемане на тази разлика, ще ви спестят пренаписване на оформлението на по-късен етап, когато числата спрат да съвпадат.

Основното извикване за обекта на страницата е TextOut(X, Y, Angle, Text). X и Y определят местоположението на текста в пунктове от долния ляв ъгъл, а Angle го завърта в градуси. По този начин се изчертава диагонален печат „DRAFTâ€?или „COPYâ€?без нужда от специална поддръжка. Трикът, който позволява на натрупаната VCL интуиция да продължи да работи, е да изразите Y като височината на страницата минус разстоянието, което искате да имате от горния край:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // 50pt from top of Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // rotated stamp
    Pdf.AddPage;                                                // CurrentPage now points here
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // font state does not carry over
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Двете промени в състоянието в този пример са отговорни за повечето грешки, които се появяват едва на втората страница. AddPage насочва CurrentPage към току-що създадената страница, така че референция към страница, която сте кеширали по-рано, вече няма да изчертава там, където очаквате. Изборът на шрифт също се прави за всяка страница поотделно, а не за целия документ. Ако пропуснете извикването на SetFont след AddPage, първото извикване на TextOut на новата страница ще се върне към параметрите по подразбиране, а не към получерния шрифт за заглавия, който сте задали преди три страници. Сигурният навик е да третирате „създаванÐ?на нова страницаâ€?и „възстановяванÐ?на настройките на текстаâ€?като една неделима стъпка в цикъла за отчети.

Шрифтове, които съществуват на сървъра, а не само на вашия компютър

Повечето проблеми с шрифтовете всъщност са проблеми с внедряването (deployment). Вашият компютър за разработка има инсталиран корпоративния шрифт, така че отчетът изглежда правилно на екрана ви. Производственият сървър обаче изпълнява задачата под сервизен акаунт, на който този шрифт никога не е инсталиран. Четецът тихомълком го замества с нещо друго, което успее да намери, а първото известие за проблема пристига от клиент, който пита защо фирмената бланка се е променила. Решението е да спрете да разчитате на директорията с шрифтове на операционната система и да заредите шрифта от файл, който инсталаторът ви поставя на диска. Методът за регистрация на Unicode в HotPDF приема път до файл и прави точно това:

Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test âœ?));

TextOut приема директно WideString, което е по-важно, отколкото изглежда на пръв поглед. Име на клиент с акцент, немска улица, полски град: това не са изключения, а стандартно съдържание на клиентска таблица. Те преминават през същото извикване, което използвате за текстовите етикети (ASCII labels), стига регистрираният шрифт действително да съдържа съответните символи (glyphs). Едно ограничение за версиите съпътства вградените шрифтове: документът трябва да бъде версия PDF 1.5 или по-нова. Ако друго изискване ви принуждава да използвате по-стара версия, тази функция просто няма да работи. Писменостите, които се четат от дясно на ляво (като арабски и иврит), се нуждаят от реално оформяне (shaping), а не просто от търсене на символи, и имат свой собствен процес на обработка. Вижте статията за оформяне на сложен текст в HotPDF.

Когато никой инсталиран шрифт не може да изрази това, което ви трябва (например MICR символи върху банкови чекове или патентован набор от символи), шрифтовете от Type 3 попълват празнината. Вие дефинирате всеки символ като малък поток от съдържание (content stream) чрез RegisterType3Font и AddType3Glyph. Това е специализирана част от API и рядко ще се налага да я използвате, но е много по-чист подход от разпръскването на стотици малки растерни изображения на символи из страницата.

Изображения: средните аргументи са ширина и височина, а не ъгъл

Обработката на изображения се разделя на две стъпки и поддържането им разделени е основната цел. Методът AddImage приема TBitmap или TJPEGImage, вгражда го веднъж и връща неговия индекс. Изображенията във формат PNG трябва да бъдат декодирани до bitmap, преди да достигнат до него. Методът ShowImage изчертава този индекс навсякъде и толкова често, колкото искате. Подредбата на аргументите в ShowImage е едно от нещата, на които си струва да обърнете внимание:

var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // decode PNG to a bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless for flat-color art
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): not (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Двете числа след позицията са ширина и височина. Те не са координатите на срещуположния ъгъл, а последният аргумент е ъгъл на завъртане в градуси. Ако прочетете сигнатурата на метода като кутия с координати X1/Y1/X2/Y2 box и логото с размери 120 на 40, поставено на (50, 700), ще се разпъне оттам до (120, 40), разпростирайки се върху по-голямата част от страницата. Резултатът прави грешката очевидна, докато изходният код изглежда съвсем разумен, което е причината за загуба на време. Свойството KeepImageAspectRatio е зададено на True по подразбиране, така че рамка с грешни пропорции просто ще добави празни полета около изображението, вместо да го деформира. Променете го на False само когато наистина искате да разтеглите изображението.

Разделянето между регистриране и позициониране носи ползи при големи обеми от работа. Тъй като AddImage вгражда пикселите веднъж и всяко извикване на ShowImage с този индекс препраща към същия вграден обект, мястото, където извиквате AddImage, определя размера на файла. Извикайте го вътре в цикъла за страници на отчет от 500 страници и едно и също лого ще бъде вградено 500 пути. Извикайте го веднъж преди цикъла, запазете индекса и логото ще бъде съхранено само веднъж. Малък речник (dictionary), индексиран по пътя на ресурса, е достатъчен, за да гарантира, че всяко отделно изображение се регистрира точно веднъж.

Изборът на кодек е другият фактор, влияещ на размера. Фотографското съдържание, сканираните прикачени файлове и подобни материали са подходящи за JPEG: предайте icJpeg на AddImage и намалете JpegQuality до около 85, тъй като свойството започва от 100 по подразбиране, а разликата при 85 е незабележима на отпечатана страница. Изображенията с плътни цветове (като лога, графики и чертежи) са подходящи за icFlate, където компресирането без загуби (lossless compression) вече е компактно, докато JPEG би добавил шум (ringing) около острите ръбове. Процес на обработка, който добавя снимка с максимално качество на всяка страница, може да увеличи размера на файловете до гигабайти. Същото съдържание при качество JPEG 85 заема около една десета от този размер, без читателят да забележи разлика.

Линии, правоъгълници и щриховане с векторни елементи

Хоризонталната линия под заглавието на таблица и сивата рамка зад крайната сума не трябва да бъдат растерни изображения. Начертайте ги като векторни елементи и те ще останат ясни при всякакъв мащаб, ще се отпечатват отчетливо и няма да добавят почти нищо към размера на файла. HotPDF следва модела на стандартните PDF потоци от съдържание (content streams): изгражда се път (path), след което се извиква оператор за оцветяването му.

// Horizontal rule under the table header
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Shaded totals box: X, Y, width, height
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

Редът на операциите е задължителен: задайте състоянието за оцветяване, изградете пътя и след това извикайте Stroke или Fill. Път, който сте изградили, но никога не сте оцветили, не добавя нищо към страницата â€?това е отговорът в повечето случаи, когато някоя линия не се показва. SetRGBFillColor приема една стойност TColor, така че познатите VCL константи като clNavy и clBlack могат да се използват директно, а Rectangle използва същите аргументи за ширина и височина, както при позиционирането на изображения, а не два срещуположни ъгъла. Едно предупреждение за тънките линии: всичко под около половин пункт може да изглежда добре на монитор, но да изчезне при печат на офис принтер с 600 dpi. Ето защо 0.75 пункта е разумен минимум за всяка линия, която трябва да се вижда при печат.

Странициране спрямо реални данни, а не примерни данни

Една подробност, която трябва да уточните преди оформянето на документа: числовите колони трябва да бъдат подравнени отдясно. Начинът да направите това е да измерите ширината на визуализираната стойност и да я позиционирате обратно от границата на колоната, а не да запълвате низа с интервали отпред. Запълването с интервали работи само при шрифтове с фиксирана ширина (monospaced fonts), а никой не оформя финансов отчет с такъв шрифт. Прекарайте стойностите първо през специфичните за локала функции на Delphi (като например FormatFloat), така че разделителят на хилядите, чиято ширина измервате, да бъде същият, който действително ще се покаже според локала на клиента.

Опасността при страницирането е, че го пишете за демо набор от данни, където десет кратки реда се събират на една страница и цикълът никога не трябва да се прекъсва. В реална среда обаче можете да получите клиент, чието име на компания е с дължина 140 символа, и отчет с 4000 позиции, при което цикълът трябва да се прекъсва правилно всеки път. Моделът, който работи успешно, използва един-единствен Y курсор, който се движи надолу, докато изваждате височината на всеки ред, и проверка, която стартира нова страница в момента, в който курсорът премине долното поле. Движението надолу тук означава намаляване на стойността на Y, което е единственото място, където началото на координатната система в долния ляв ъгъл остава неинтуитивно. Дръжте целия този процес в една подпрограма, която също така преиздава SetFont и изчертава отново заглавната част (running header) на новата страница, за да предотвратите грешки с изрязване на страници. Когато същите отчети трябва да отговарят и на правилата за архивиране или достъпност, изборите, които правите тук (кои шрифтове да вградите, дали резултатът е с тагове, кои цветови пространства да използвате), са именно тези, които се контролират от съответните стандарти; ръководството за PDF/A, PDF/X и PDF/UA в HotPDF си струва да бъде прочетено преди оформянето на финалния шаблон.

Всяко представено тук извикване â€?позиционирането на текст, регистрацията на шрифтове, вграждането на изображения и изчертаването на пътища â€?се доставя в компонента HotPDF за Delphi и C++Builder, чийто справочник описва пълния приложен програмен интерфейс (API) заедно с функциите за формуляри, шифриране и подписване.