HotPDF рисует векторную графику, строя путь на текущей странице, а затем запрашивая его отрисовку. Никакого промежуточного растрового этапа между ними нет. Линия, нарисованная через MoveTo и LineTo, оказывается операторами пути PDF в потоке содержимого, поэтому она остается настоящим вектором: четкой при масштабе 50%, четкой при 1600% и занимает лишь долю от того, во что обошелся бы растрированный вариант. Для диаграмм, разделительных линий таблиц, осей графиков и оформления форм это ровно то, что нужно, а стоящий за этим API достаточно мал, чтобы освоить его за один присест
Вся поверхность рисования живет на THotPDF.CurrentPage. Между BeginDoc и EndDoc вы задаете цвет и толщину линии на этом объекте страницы, выкладываете геометрию и вызываете оператор отрисовки, чтобы зафиксировать ее. Четыре примитива, которыми вы будете пользоваться чаще всего, — это MoveTo и LineTo для произвольных путей, Rectangle для прямоугольников, Circle для кругов и два оператора отрисовки Stroke и Fill
Система координат — с началом внизу слева
Это единственное, на чем спотыкаются все, кто приходит из VCL. TCanvas, которым вы рисуете элементы управления, помещает начало координат в верхний левый угол с ростом Y вниз. В PDF все наоборот. HotPDF отсчитывает от нижнего левого угла страницы в пунктах (1/72 дюйма), причем Y растет по мере движения вверх. Точка при Y := 720 находится у верха страницы US Letter, высота которой 792 пункта, а Y := 50 — у нижнего края. Если ваш первый рисунок вышел отраженным по вертикали, причина именно в этом: код, перенесенный из экранной графики, предполагает неверное направление и уходит за нижний край
То же соглашение действует для TextOut, поэтому текст и фигуры разделяют одну ментальную модель, стоит только ее усвоить. Планируйте компоновку, решая, где находится низ каждого элемента, а не верх, и все остальное следует за этим
Пути: MoveTo, LineTo, Stroke
Обведенный путь — это перо, поднятое, поставленное и протянутое. MoveTo поднимает перо и задает начальную точку, ничего не отмечая. Каждый LineTo продлевает текущий путь до новой точки. На странице ничего не появляется, пока вы не вызовете Stroke, который рисует накопленный путь текущим цветом обводки и толщиной линии, а затем очищает путь, чтобы следующий MoveTo начинался с чистого листа
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'DrawPaths.pdf';
Pdf.BeginDoc;
// Line width is in points and applies until you change it.
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
// A horizontal rule near the top of the page (Y measured from bottom).
Pdf.CurrentPage.MoveTo(72, 720);
Pdf.CurrentPage.LineTo(523, 720);
Pdf.CurrentPage.Stroke; // commit the path; nothing drew before this
// A thicker connected polyline: three segments in one path.
Pdf.CurrentPage.SetLineWidth(3);
Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
Pdf.CurrentPage.MoveTo(72, 640);
Pdf.CurrentPage.LineTo(172, 690);
Pdf.CurrentPage.LineTo(272, 620);
Pdf.CurrentPage.LineTo(372, 680);
Pdf.CurrentPage.Stroke;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Две детали экономят реальное время на отладку. Толщина линии — это состояние, а не аргумент: SetLineWidth задает ее один раз, и каждый последующий Stroke использует это значение, пока вы не измените его снова, — вот почему ломаная линия выше толще разделительной. А путь сбрасывается после каждого Stroke, так что забытый Stroke означает, что так тщательно выложенная геометрия вообще не отрисуется. Если фигура отсутствует в результате, вызов отрисовки — первое, куда стоит заглянуть
Координаты — это пункты, а пункты дробны. MoveTo и LineTo принимают значения типа Single, так что волосяная линия в 0.5 пункта или позиция в 72.25 допустима и осмысленна, а не округлена до ближайшей целой единицы. Эта точность важна в двух противоположных направлениях. Толщина линии ниже примерно 0.5 может отрисоваться как зависящая от устройства предельно тонкая линия, которая исчезает на экране и вновь появляется при печати, поэтому видимой разделительной линии нужна толщина, заданная намеренно, а не по умолчанию. С другой стороны, привязка разделительных линий таблиц и линий сетки к целочисленным координатам в пунктах не дает плотной сетке выглядеть слегка неровной там, где соседние линии округляются по-разному. Определите шаг сетки в пунктах заранее, и вся остальная компоновка унаследует его
Залитые фигуры и цвет
Замкнутые примитивы можно залить вместо обводки. Rectangle принимает позицию и размер, Circle принимает центр и радиус, и любой из них фиксируется через Fill, который заливает внутреннюю область текущим цветом заливки, или через Stroke — для одного лишь контура. Цвет заливки и цвет обводки — это отдельные части состояния, задаваемые через SetRGBFillColor и SetRGBStrokeColor, каждый из которых принимает единственный TColor. Это значит, что вы можете напрямую переиспользовать цветовые константы Delphi и вспомогательную функцию RGB
// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;
// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;
// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;
Следите за формой аргументов у Rectangle. Это позиция-плюс-размер, X, Y, Width, Height, а не два противоположных угла. Знакомый Delphi-разработчикам TCanvas.Rectangle принимает (Left, Top, Right, Bottom), поэтому мышечная память передаст HotPDF второй угол там, где он ожидает ширину и высоту, и прямоугольник выйдет неверного размера. Пара (X, Y) — это нижний левый угол, согласованный с началом координат страницы. Для круга (X, Y) — это центр, а третий аргумент — радиус в пунктах
Одно ошибочное решение по цвету в исходном примере
В более старой версии этого примера цвета инициализировались через Random($FFFFFF) на каждой фигуре. Выглядит живо, но для генерируемых документов это неверный инстинкт. PDF, который вы собираете из кода, обычно является чем-то, что вы также хотите тестировать, а случайные цвета заливки делают вывод невозможным для сравнения от запуска к запуску: побайтовое сравнение с заведомо корректным файлом проваливается каждый раз без всякой реальной причины. Выбирайте явные цвета. Когда вам нужно разнообразие по серии фигур, управляйте им из ваших данных или фиксированного массива-палитры, чтобы один и тот же вход всегда давал один и тот же файл. Детерминизм ценнее новизны, когда артефакт проходит через релизный конвейер
Собираем примитивы вместе: выносной блок
Каждый примитив прост сам по себе; отдача проявляется, когда горстка их складывается во что-то, что действительно нужно отчету. Выноска — снабженный пояснением блок, который указывает на фигуру и объясняет ее, — использует все, что рассмотрено до сих пор: залитый прямоугольник с рамкой, обведенная линия-указатель, точка для закрепления указателя и текст, размещенный внутри блока в тех же координатах с началом внизу слева, что и у фигур. FillAndStroke здесь заслуживает своего места, отрисовывая внутреннюю область и контур одного пути за одну фиксацию вместо того, чтобы строить прямоугольник дважды
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'Callout.pdf';
Pdf.BeginDoc;
// 1. The box: pale fill plus a visible border, one path, one commit.
// Rectangle is lower-left corner plus size, Y measured from the bottom
Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214)); // pale amber panel
Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40)); // darker rim
Pdf.CurrentPage.SetLineWidth(1);
Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
Pdf.CurrentPage.FillAndStroke;
// 2. The pointer: one stroked segment from the box edge down
// toward the thing being annotated
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.MoveTo(90, 615); // left edge of the box
Pdf.CurrentPage.LineTo(66, 546);
Pdf.CurrentPage.Stroke;
// 3. A filled dot anchors the pointer at its target
Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
Pdf.CurrentPage.Circle(64, 542, 3);
Pdf.CurrentPage.Fill;
// 4. The label, positioned relative to the box's lower-left corner.
// Text and shapes share one coordinate system, so the offsets
// are plain arithmetic against (90, 600)
Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Обратите внимание, как мало управления состоянием нужно этой композиции. Цвет заливки, цвет обводки и толщина линии задаются непосредственно перед фигурой, которая их использует, поэтому каждый блок рисунка читается как самодостаточная единица, и его можно переставить или вынести во вспомогательную процедуру, не таща за собой скрытое состояние. Оберните это в процедуру, принимающую точку привязки и строки, и вы получите переиспользуемую аннотацию для диаграмм ценой сорока строк
Где векторное рисование окупается, а где нет
Тянитесь к этим вызовам путей и фигур, когда геометрия генерируется: линии сетки и столбцы графиков, разлинованные строки таблицы счета, выносные блоки на диаграмме, знак логотипа, выраженный горсткой путей. Все это масштабируется без размытия и почти ничего не добавляет к размеру файла, потому что прямоугольник — это несколько чисел, а не тысячи пикселей. Обратная сторона тоже честна. Если у вас на руках на самом деле фотография или скриншот, вместо этого рисуйте его как изображение через AddImage и ShowImage; трассировка растра векторными вызовами не даст вам ничего. Прямые отрезки, прямоугольники и круги выше несут подавляющее большинство реальной работы по отчетности, а три уточнения, о которых разработчики спрашивают следующими, — кривые, штриховые узоры и прозрачность — располагаются на том же объекте страницы
Кривые, штрихи и прозрачность вкратце
Произвольные кривые расширяют тот же механизм путей, что у вас уже есть. CurveToC(X1, Y1, X2, Y2, X3, Y3) добавляет сегмент кубической кривой Безье от текущей точки к (X3, Y3), изгибаясь к двум контрольным точкам, а сокращенные варианты CurveToV и CurveToY покрывают случаи, когда одна контрольная точка совпадает с конечной. Путь может свободно смешивать сегменты LineTo и CurveToC перед тем, как единственный Stroke или Fill зафиксирует его, — так строятся скругленные углы и плавные линии графиков
Штриховые обводки — это состояние, ровно как толщина линии. SetDash([3, 3], 0) переключает каждую последующую обводку на узор «три пункта штрих, три пункта пробел», где массив прописывает длины отрезков штриха и пробела в пунктах, а второй аргумент задает фазу, где начинается цикл; NoDash возвращает перо к сплошной линии. Задайте его, обведите линии сетки, которым он нужен, и сбросьте перед следующей сплошной разделительной линией, иначе штрих тихо заразит все, что следует дальше
Прозрачность проходит через именованное графическое состояние, а не через аргумент цвета, потому что альфа в PDF — это свойство словаря графического состояния. Зарегистрируйте одно на документе через RegisterExtGState, передав альфу заливки и альфу обводки между 0 и 1, затем примените возвращенное имя через CurrentPage.SetGraphicsState; заливки и обводки с этого момента отрисовываются с зарегистрированной непрозрачностью. Это более тяжеловесная церемония, чем сеттеры цвета, и она того стоит в первый же раз, когда полоса выделения должна лечь поверх текста, не скрывая его
Оставшаяся привычка, которую стоит хранить, — это проверка. Генерируемая геометрия может пройти на вашей машине и провалиться на машине клиента, обычно из-за подмены шрифтов в любом тексте, который вы примешиваете, или из-за предположения о размере страницы, которое не выполняется. Откройте готовый файл на нескольких уровнях масштаба, чтобы убедиться, что края остаются чистыми, и проверьте, что каждая фигура попадает внутрь задуманного вами поля полей. При детерминированной цветовой схеме эту проверку можно автоматизировать против эталонного PDF, а не осматривать глазами
Показанные здесь вызовы MoveTo, LineTo, Stroke, Fill и цвета входят в состав HotPDF Component для Delphi и C++Builder