Columns, drop caps and wrapping
This page covers newspaper columns, drop caps, pull quotes and sidebars. Two engines do this work, and you choose by how you build the page:
XTextFormatter, in the corePdfPinatapackage (namespacePdfPinata.Drawing.Layout), lays text out in a rectangle you give it on anXGraphics. It can split the rectangle into columns, set a drop cap, and keep text clear of rectangles you name. Use it when you place things on the page yourself.- PinataLayout lays out a whole document. A text frame, image or chart can ask for the text beside it to run down one side. Use it when the renderer decides where things go.
A PinataLayout section has no column setting. For multi-column text, use XTextFormatter.
Set text in columns
Set Columns and ColumnGap on the formatter. It divides the layout rectangle into that many columns
of equal width and fills each in turn. ColumnGap is in points, and the default is 18.
const int columnCount = 5;
const double columnGap = 14;
var columnWidth = (measure - columnGap * (columnCount - 1)) / columnCount;
var body = new XFont(Serif, 9.5);
formatter.Columns = columnCount;
formatter.ColumnGap = columnGap;
formatter.Alignment = XParagraphAlignment.Justify;
Then draw the text into the whole rectangle:
// Enough copy to fill five columns twice over. A story that runs out halfway leaves
// empty columns, which says nothing about how the formatter fills them.
var story = string.Concat(Copy, Copy, Copy, Copy, Copy, Copy, Copy, Copy);
formatter.DrawString(story, body, XBrushes.Black,
new XRect(margin, upperTop, measure, upperHeight));
The formatter draws nothing in the gutters. To draw a rule down each one, work out the gutter positions with the same sums the formatter uses:
// The formatter draws no rules, so the gutter centres are worked out with the same
// arithmetic it used to place the columns. Getting this wrong is how a rule ends up
// through the middle of a column rather than between two.
var gutter = new XPen(XColors.LightGray, 0.5);
for (var index = 1; index < columnCount; index++)
{
var x = margin + index * (columnWidth + columnGap) - columnGap / 2;
gfx.DrawLine(gutter, x, upperTop, x, upperTop + upperHeight);
gfx.DrawLine(gutter, x, lowerTop, x, lowerTop + lowerHeight);
}
The formatter keeps its settings between calls. Set Columns back to 1 before you draw a caption or
a headline with the same formatter.
Open with a drop cap
A drop cap is one property. Give DropCap an XDropCap with a font and a depth in lines. The
formatter takes the first character of the text, scales it so its foot sits on the baseline of the
last line it spans, and shortens those lines to leave room for it.
// The cap is one property. The formatter takes the first character of the text,
// scales it so that its foot rests on the baseline of the third line, reserves the
// room beside it and shortens the three lines that sit against it. The size is not
// given here: a depth in lines is what the surrounding text is measured in, and a
// size would imply a depth that is almost never a whole number of them.
formatter.DropCap = new XDropCap(new XFont(Serif, 10, XFontStyle.Bold), lines: 3);
formatter.Alignment = XParagraphAlignment.Justify;
formatter.Columns = 2;
formatter.ColumnGap = 18;
// One call for the whole feature: the cap, the lines that clear it, and the two
// columns the rest of it flows down.
formatter.DrawString(string.Concat(Opening, Body, Body, Body, Body, Body), body,
XBrushes.Black, new XRect(margin, textTop, measure, height - textTop - margin - 20));
formatter.DropCap = null;
formatter.Columns = 1;
formatter.Alignment = XParagraphAlignment.Left;
The size of the font you pass is ignored: the formatter works out the size from the depth. The font
gives only the family and the style. XDropCap.Gutter sets the space between the cap and the text,
in points; when you leave it null, the space is one space character of the body font.
The cap takes only the first character, so write the text in full. In a multi-column block, the cap takes room in the first column only.
Flow text around a pull quote or picture
To keep text off something you have drawn in the block, add a RectangleObstacle to the formatter's
Obstacles. The lines level with the obstacle are shortened to clear it, in every column it stands
in. The lines above and below it run the full width. One DrawString call lays out the whole block.
var textTopOfPage = margin + 86;
var textHeight = height - textTopOfPage - margin - 20;
// Positioned relative to the layout rectangle, which is what an obstacle is measured
// in - so the page coordinates the quote is drawn at are these plus the block's corner.
var quoteInBlock = new XRect(140, 150, measure - 280, 108);
var quote = new XRect(margin + quoteInBlock.X, textTopOfPage + quoteInBlock.Y,
quoteInBlock.Width, quoteInBlock.Height);
// Set on a tint, at a slight slant. ObliqueAngle skews the glyphs where a real
// italic would redraw them, which is the honest tool for display type that has no
// italic of its own to reach for.
gfx.DrawRectangle(new XSolidBrush(XColor.FromArgb(255, 248, 232)), quote);
gfx.DrawLine(new XPen(XColors.DarkSlateGray, 2), quote.X, quote.Y,
quote.X, quote.Y + quote.Height);
formatter.Alignment = XParagraphAlignment.Justify;
formatter.Columns = 2;
formatter.ColumnGap = 18;
// The padding is the obstacle's own, because how much air a thing wants around it is a
// fact about that thing rather than about the text.
formatter.Obstacles.Add(new RectangleObstacle(quoteInBlock, padding: 14));
// Enough copy to fill both columns, because a column left empty would show nothing about
// an obstacle standing in it.
formatter.DrawString(
string.Concat(Enumerable.Repeat(Body, 17)), body,
XBrushes.Black, new XRect(margin, textTopOfPage, measure, textHeight));
formatter.Obstacles.Clear();
The obstacle's rectangle is relative to the layout rectangle, not to the page: (0, 0) is the top
left corner of the block. To draw the quote itself, add the block's corner to get page coordinates,
as the demo does with quoteInBlock and quote.
The padding argument keeps text that distance from the obstacle on all four sides. Each obstacle
has its own padding.
When an obstacle stands clear of both edges of a column, a line has room on both sides of it. The formatter fills the wider side and leaves the narrower one empty. It never splits one line across an obstacle. The pull quote in the demo straddles the gutter, so each column keeps its outside edge.
Keep text inside a box
A word wider than its column is drawn past the column's edge, and a layout rectangle can reach past
the box you drew around it. To cut text off at the edge of a box, clip to the box. XGraphics has no
way to undo a clip except to restore a state saved before it:
// IntersectClip has no counterpart to undo it - there is no ResetClip - so the only
// way back is to restore a state saved before it was narrowed.
var sidebar = new XRect(margin, pictureTop, columnWidth, pictureHeight);
var state = gfx.Save();
gfx.DrawRectangle(new XSolidBrush(XColor.FromArgb(255, 250, 235)), sidebar);
gfx.DrawRectangle(new XPen(XColors.Black, 0.8), sidebar);
gfx.IntersectClip(sidebar);
var boxed = new XTextFormatter(gfx);
boxed.DrawString("ALSO INSIDE", new XFont(Sans, 8, XFontStyle.Bold), XBrushes.Black,
new XRect(sidebar.X + 8, sidebar.Y + 8, sidebar.Width - 16, 14));
boxed.DrawString(
"This box is clipped to its own rectangle, so the long paragraph inside it is "
+ "cut off at the edge rather than running over the column beside it. The clip "
+ "is undone by restoring the graphics state, because there is nothing else "
+ "that will undo it.",
new XFont(Serif, 8.5), XBrushes.Black,
new XRect(sidebar.X + 8, sidebar.Y + 26, sidebar.Width - 16, sidebar.Height));
gfx.Restore(state);
Run text beside a shape in a PinataLayout document
In a PinataLayout document, set WrapFormat.Style on a shape to one of the four side-wrap styles. The
paragraphs after the shape are laid out beside it: lines level with the shape are shortened, and lines
above and below it run the full width. You do not split or measure the text.
// The frame is added to the flow like any other element. RelativeVertical.Paragraph is
// what makes it float at all: a shape anchored to the page or the margin is placed
// absolutely and the text is laid out as though it were not there.
var frame = section.AddTextFrame();
frame.Width = Unit.FromCentimeter(4.5);
frame.Height = Unit.FromCentimeter(4);
frame.RelativeVertical = RelativeVertical.Paragraph;
frame.RelativeHorizontal = RelativeHorizontal.Margin;
if (where.HasValue)
frame.Left = where.Value;
else
frame.Left = Unit.FromCentimeter(1.2);
frame.FillFormat.Color = new Color(246, 243, 234);
frame.LineFormat.Width = 0.75;
frame.LineFormat.Color = Colors.DarkSlateGray;
frame.MarginTop = Unit.FromPoint(8);
frame.MarginLeft = Unit.FromPoint(10);
frame.MarginRight = Unit.FromPoint(10);
frame.WrapFormat.Style = style;
// All four distances mean something for a side-wrapped shape. Left and Right hold the
// text off horizontally, as they always claimed to; Top and Bottom grow the obstacle
// vertically, so a line whose box would otherwise clear the frame by a hair is pushed
// past it instead of grazing it.
frame.WrapFormat.DistanceLeft = Unit.FromPoint(10);
frame.WrapFormat.DistanceRight = Unit.FromPoint(10);
frame.WrapFormat.DistanceTop = Unit.FromPoint(4);
frame.WrapFormat.DistanceBottom = Unit.FromPoint(4);
WrapStyle | Where the text goes |
|---|---|
Right | Down the right of the shape. Put the shape at the left. |
Left | Down the left of the shape. Put the shape at the right. |
Largest | Down whichever side has more room. |
Both | Either side. It lays out the same as Largest. |
TopBottom | Above and below the shape, never beside it. This is the default. |
None, Through | The text ignores the shape and can run over it. |
Left and Right name the side the text takes, not the side the shape sits on. If you get it
backwards, nothing fails and the page still looks deliberate.
The four WrapFormat distances hold the text off the shape. DistanceLeft and DistanceRight keep
the text away at the sides. DistanceTop and DistanceBottom make the shape taller for wrapping, so
a line that would clear it by a hair moves past it instead.
Things to know
XTextFormatterdoes not tell you what did not fit. Text that runs past the last column is dropped, and no call returns the rest. For text that must continue in another block, use obstacles to keep it in one block, or use PinataLayout, which breaks pages for you.- Obstacles and
Rotationdo not mix. If a formatter has obstacles and aRotationthat is not zero,DrawStringandGetLayoutthrowInvalidOperationException. To turn a block that has obstacles, leaveRotationat zero and rotate theXGraphicsbefore you draw. - A null in
Obstaclesthrows. Remove an entry rather than leaving a null in the list. - Clear the formatter after use.
DropCapandObstaclesstay set for the nextDrawStringcall. SetDropCapto null and callObstacles.Clear()when you are done. - A drop cap looks best with a glyph outline provider. When
GlobalFontSettings.GlyphOutlineProvideris set (the backends supplySkiaGlyphOutlineProviderandImageSharpGlyphOutlineProvider), the cap sits flush with the margin by its ink. Without one it is placed by its advance width, which leaves a small gap. Nothing throws either way. - Obstacles are rectangles. Text does not follow the outline of a picture, in either engine.
- A shape floats only when anchored to the text. Side wrapping needs
RelativeVerticalset toParagraphorLine. A shape anchored to thePageorMarginis placed at a fixed position, and the text is laid out as if it were not there. - Side wrapping is for paragraphs. Wrapping a table beside a shape is not supported.
- A shape that does not fit falls back. When a side-wrapped shape is taller than the room left on
the page, or would cross a page break, it is laid out as
TopBottominstead. - Side-wrap styles need a matching reader. A document saved as DDL with
Left,Right,LargestorBothcan be read only by a version of this library that has those styles.
See it in action
- The Newspaper demo sets a front page in five justified columns with gutter rules and a clipped sidebar.
- The Magazine demo opens a feature with a drop cap in two columns, and flows the next page around a pull quote.
- The SideWrap demo puts a text frame beside a paragraph with each of the four side-wrap styles, one per page.
The full Newspaper demo
const string Serif = "Liberation Serif";
const string Sans = "Liberation Sans";
const string Copy =
"Readers of this page will notice that the text is set in five columns of equal "
+ "width, justified, with a rule down each gutter. None of that is automatic. The "
+ "formatter is given one rectangle and told how many columns to divide it into, "
+ "and it fills each in turn before moving on to the next. Where the columns meet "
+ "is arithmetic the caller has to repeat if it wants to draw anything there. "
+ "The photograph below is not flowed round either. Nothing in the library wraps "
+ "text about an object, so the story is drawn twice - once into the space above "
+ "the picture and once into the space below it - and the break between them is "
+ "chosen by measuring rather than by counting words. It is more work than a "
+ "single call, and it is the honest amount of work for what is being asked. ";
var document = new PdfDocument();
document.Info.Title = "The Daily Broadsheet";
var page = document.AddPage();
page.Size = PageSize.A3;
var gfx = XGraphics.FromPdfPage(page);
var formatter = new XTextFormatter(gfx);
var width = page.Width.Point;
const double margin = 40;
var measure = width - margin * 2;
// ---- Masthead ------------------------------------------------------------------
// Letterspacing a masthead is what CharacterSpacing is for. At display sizes the
// default fit is too tight, and the gap has to be opened by hand.
gfx.DrawString("THE DAILY BROADSHEET", new XFont(Serif, 46, XFontStyle.Bold),
XBrushes.Black, new XRect(margin, 46, measure, 56),
new XStringFormat
{
Alignment = XStringAlignment.Center,
CharacterSpacing = 2.5
});
gfx.DrawLine(new XPen(XColors.Black, 2.4), margin, 106, width - margin, 106);
gfx.DrawLine(new XPen(XColors.Black, 0.6), margin, 111, width - margin, 111);
var folio = new XFont(Sans, 8);
gfx.DrawString("Wednesday 12 August 2026", folio, XBrushes.Black,
new XRect(margin, 118, measure, 12), XStringFormats.TopLeft);
gfx.DrawString("No. 41,208", folio, XBrushes.Black,
new XRect(margin, 118, measure, 12), XStringFormats.TopCenter);
gfx.DrawString("Two pounds", folio, XBrushes.Black,
new XRect(margin, 118, measure, 12), XStringFormats.TopRight);
gfx.DrawLine(new XPen(XColors.Black, 0.6), margin, 134, width - margin, 134);
// ---- Headline ------------------------------------------------------------------
formatter.Alignment = XParagraphAlignment.Center;
formatter.DrawString("Library gains columns, wraps nothing round anything",
new XFont(Serif, 32, XFontStyle.Bold), XBrushes.Black,
new XRect(margin, 152, measure, 80));
formatter.DrawString(
"Five columns, a rule in every gutter, and a photograph the story declines to "
+ "flow around",
new XFont(Serif, 13, XFontStyle.Italic), XBrushes.Black,
new XRect(margin, 224, measure, 40));
formatter.Alignment = XParagraphAlignment.Left;
gfx.DrawLine(new XPen(XColors.Black, 0.6), margin, 268, width - margin, 268);
gfx.DrawString("By a Staff Reporter", new XFont(Sans, 9, XFontStyle.Bold),
XBrushes.Black, new XPoint(margin, 288));
// ---- The body, in columns ------------------------------------------------------
const int columnCount = 5;
const double columnGap = 14;
var columnWidth = (measure - columnGap * (columnCount - 1)) / columnCount;
var body = new XFont(Serif, 9.5);
formatter.Columns = columnCount;
formatter.ColumnGap = columnGap;
formatter.Alignment = XParagraphAlignment.Justify;
// The story is flowed twice: once above the photograph, once below it. There is no
// wrap-around-object anywhere in the library, so the space beside a picture has to
// be given to the formatter as a rectangle that does not include the picture.
const double upperTop = 300;
const double upperHeight = 250;
var pictureTop = upperTop + upperHeight + 16;
// Enough copy to fill five columns twice over. A story that runs out halfway leaves
// empty columns, which says nothing about how the formatter fills them.
var story = string.Concat(Copy, Copy, Copy, Copy, Copy, Copy, Copy, Copy);
formatter.DrawString(story, body, XBrushes.Black,
new XRect(margin, upperTop, measure, upperHeight));
// ---- Photograph, spanning the middle columns -----------------------------------
using var photograph = XImage.FromStream(
() => Assets.Open(Assets.ImagePrefix + "pdf-pinata.jpg"));
// The picture takes columns two to four of its band, leaving the first column of
// that band for the side story. Both sit between the two blocks of body text
// rather than over them, which is what keeps them from being drawn on top of.
var pictureLeft = margin + (columnWidth + columnGap);
var pictureWidth = columnWidth * 3 + columnGap * 2;
var pictureHeight = pictureWidth * photograph.PointHeight / photograph.PointWidth;
gfx.DrawImage(photograph, pictureLeft, pictureTop, pictureWidth, pictureHeight);
var caption = new XFont(Sans, 7.5, XFontStyle.Italic);
formatter.Columns = 1;
formatter.Alignment = XParagraphAlignment.Left;
formatter.DrawString(
"Two readers consider the gutter rules, which are drawn rather than provided.",
caption, XBrushes.DimGray,
new XRect(pictureLeft, pictureTop + pictureHeight + 4, pictureWidth, 24));
// ---- The rest of the story, below the photograph -------------------------------
var lowerTop = pictureTop + pictureHeight + 30;
// Floored at zero. Everything above is computed from the photograph's own aspect
// ratio, so a taller picture pushes this down the page, and a rectangle with a
// negative height throws rather than drawing nothing. Swapping the image should
// give a worse looking page, not an exception.
var lowerHeight = Math.Max(0, page.Height.Point - lowerTop - margin - 24);
formatter.Columns = columnCount;
formatter.Alignment = XParagraphAlignment.Justify;
formatter.DrawString(story, body, XBrushes.Black,
new XRect(margin, lowerTop, measure, lowerHeight));
formatter.Columns = 1;
formatter.Alignment = XParagraphAlignment.Left;
// ---- Gutter rules ---------------------------------------------------------------
// The formatter draws no rules, so the gutter centres are worked out with the same
// arithmetic it used to place the columns. Getting this wrong is how a rule ends up
// through the middle of a column rather than between two.
var gutter = new XPen(XColors.LightGray, 0.5);
for (var index = 1; index < columnCount; index++)
{
var x = margin + index * (columnWidth + columnGap) - columnGap / 2;
gfx.DrawLine(gutter, x, upperTop, x, upperTop + upperHeight);
gfx.DrawLine(gutter, x, lowerTop, x, lowerTop + lowerHeight);
}
// ---- A boxed side story, clipped ------------------------------------------------
// IntersectClip has no counterpart to undo it - there is no ResetClip - so the only
// way back is to restore a state saved before it was narrowed.
var sidebar = new XRect(margin, pictureTop, columnWidth, pictureHeight);
var state = gfx.Save();
gfx.DrawRectangle(new XSolidBrush(XColor.FromArgb(255, 250, 235)), sidebar);
gfx.DrawRectangle(new XPen(XColors.Black, 0.8), sidebar);
gfx.IntersectClip(sidebar);
var boxed = new XTextFormatter(gfx);
boxed.DrawString("ALSO INSIDE", new XFont(Sans, 8, XFontStyle.Bold), XBrushes.Black,
new XRect(sidebar.X + 8, sidebar.Y + 8, sidebar.Width - 16, 14));
boxed.DrawString(
"This box is clipped to its own rectangle, so the long paragraph inside it is "
+ "cut off at the edge rather than running over the column beside it. The clip "
+ "is undone by restoring the graphics state, because there is nothing else "
+ "that will undo it.",
new XFont(Serif, 8.5), XBrushes.Black,
new XRect(sidebar.X + 8, sidebar.Y + 26, sidebar.Width - 16, sidebar.Height));
gfx.Restore(state);
// ---- Foot ------------------------------------------------------------------------
gfx.DrawLine(new XPen(XColors.Black, 0.6), margin, page.Height.Point - margin - 14,
width - margin, page.Height.Point - margin - 14);
gfx.DrawString("The Daily Broadsheet · Wednesday 12 August 2026 · Page 1", folio,
XBrushes.Black,
new XRect(margin, page.Height.Point - margin - 10, measure, 12),
XStringFormats.TopCenter);
The full Magazine demo
const string Serif = "Liberation Serif";
const string Sans = "Liberation Sans";
// The text begins with the letter the cap is made from. The formatter takes the first
// character for the cap and lays out the rest, so the word reads whole on the page and
// the caller does not split it by hand.
const string Opening =
"There is a drop cap in this library now, and it is the property set below rather "
+ "than the thirty lines this demo used to carry. The pull quote on the next page used "
+ "to be two text blocks with a gap measured out between them; it is one block and one "
+ "obstacle now, and the copy finds its own way down both sides of it. The SideWrap "
+ "demo does the same thing a level up, for a shape in a PinataLayout document. ";
const string Body =
"The letter beside these lines is not drawn separately. DropCap says how many lines "
+ "deep it sits; the formatter scales it so its foot rests on the baseline of the "
+ "third one, reserves the room, and shortens the lines that stand against it. It is "
+ "placed by the outline of the glyph rather than by its advance, so the ink sits "
+ "flush with the margin instead of a side bearing's width inside it. ";
var document = new PdfDocument();
document.Info.Title = "Feature";
// ---- Page one: the opener --------------------------------------------------------
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);
var formatter = new XTextFormatter(gfx);
var width = page.Width.Point;
var height = page.Height.Point;
const double margin = 46;
void RunningFoot(XGraphics target, string folio) =>
target.DrawString(folio, new XFont(Sans, 8), XBrushes.Gray,
new XRect(margin, height - margin, width - margin * 2, 12),
XStringFormats.TopCenter);
using var photograph = XImage.FromStream(
() => Assets.Open(Assets.ImagePrefix + "pdf-pinata.jpg"));
// A full bleed is a rectangle that starts at the page edge and finishes past it.
// The image is scaled to cover, exactly as in the Images demo - but covering means
// one dimension overflows, and an overflow off the bottom lands on the article
// rather than off the page. So it is clipped to the band it belongs in, and the
// clip is undone by restoring the state, there being no ResetClip.
var bleedHeight = height * 0.42;
var cover = System.Math.Max(
width / photograph.PointWidth, bleedHeight / photograph.PointHeight);
var bleed = gfx.Save();
gfx.IntersectClip(new XRect(0, 0, width, bleedHeight));
gfx.DrawImage(photograph, 0, 0,
photograph.PointWidth * cover, photograph.PointHeight * cover);
// A scrim, so white type has something to sit on whatever the photograph happens to
// be doing underneath it. One gradient, from nothing at the top to nearly opaque at
// the foot of the band: a gradient honours the alpha of its colours, so this fades out
// as well as down and the picture shows through the top of it.
var scrim = new XRect(0, bleedHeight * 0.45, width, bleedHeight * 0.55);
gfx.DrawRectangle(
new XLinearGradientBrush(scrim,
XColor.FromArgb(0, 12, 14, 10), XColor.FromArgb(190, 12, 14, 10),
XLinearGradientMode.Vertical),
scrim);
gfx.Restore(bleed);
gfx.DrawString("FEATURE", new XFont(Sans, 9, XFontStyle.Bold), XBrushes.White,
new XRect(margin, bleedHeight - 128, width - margin * 2, 14),
new XStringFormat { CharacterSpacing = 3 });
formatter.DrawString("The arithmetic behind\na page that looks designed",
new XFont(Serif, 30, XFontStyle.Bold), XBrushes.White,
new XRect(margin, bleedHeight - 108, width - margin * 2, 84));
gfx.DrawString("Photographs by the test suite · Words by nobody",
new XFont(Sans, 8), new XSolidBrush(XColor.FromArgb(230, 255, 255, 255)),
new XPoint(margin, bleedHeight - 20));
// ---- The drop cap ----------------------------------------------------------------
var body = new XFont(Serif, 10);
var textTop = bleedHeight + 30;
var measure = width - margin * 2;
// The cap is one property. The formatter takes the first character of the text,
// scales it so that its foot rests on the baseline of the third line, reserves the
// room beside it and shortens the three lines that sit against it. The size is not
// given here: a depth in lines is what the surrounding text is measured in, and a
// size would imply a depth that is almost never a whole number of them.
formatter.DropCap = new XDropCap(new XFont(Serif, 10, XFontStyle.Bold), lines: 3);
formatter.Alignment = XParagraphAlignment.Justify;
formatter.Columns = 2;
formatter.ColumnGap = 18;
// One call for the whole feature: the cap, the lines that clear it, and the two
// columns the rest of it flows down.
formatter.DrawString(string.Concat(Opening, Body, Body, Body, Body, Body), body,
XBrushes.Black, new XRect(margin, textTop, measure, height - textTop - margin - 20));
formatter.DropCap = null;
formatter.Columns = 1;
formatter.Alignment = XParagraphAlignment.Left;
RunningFoot(gfx, "1");
// ---- Page two: the continuation and the pull quote --------------------------------
page = document.AddPage();
gfx = XGraphics.FromPdfPage(page);
formatter = new XTextFormatter(gfx);
gfx.DrawString("THE ARITHMETIC BEHIND A PAGE", new XFont(Sans, 8, XFontStyle.Bold),
new XSolidBrush(XColor.FromArgb(140, 140, 140)),
new XRect(margin, margin, measure, 12),
new XStringFormat { CharacterSpacing = 2 });
gfx.DrawLine(new XPen(XColors.Gainsboro, 0.6), margin, margin + 18, width - margin,
margin + 18);
// A title as geometry rather than as text. AddString puts the glyph outlines into a
// path, which can then be filled with anything a shape can be filled with - here a
// gradient across the word, which no DrawString overload can produce.
//
// It needs a glyph outline provider registered, which the runner does along with the
// other backends. To stroke a title and nothing more you would not come here at all:
// DrawString takes a pen as well as a brush, which is cheaper and stays searchable.
var titleBox = new XRect(margin, margin + 30, measure, 44);
var title = new XGraphicsPath();
title.AddString("Continued", new XFontFamily(Serif), XFontStyle.Bold, 34, titleBox,
XStringFormats.TopLeft);
gfx.DrawPath(
new XLinearGradientBrush(titleBox, XColors.DarkSlateGray, XColors.CadetBlue,
XLinearGradientMode.Horizontal),
title);
// ---- The pull quote, and the copy that flows around it -----------------------------
// One text block for the whole page and one obstacle standing in it. The quote is
// narrower than the measure and centred, so it straddles the gutter and takes the
// right of the first column and the left of the second - which leaves a usable run
// down each outside edge, and that is where the copy goes.
//
// Nothing here measures text. The two blocks with a gap arithmetic'd between them
// that this used to be needed the quote's height, the gap either side of it and the
// line height all kept in step by hand, and got them wrong whenever the font changed.
var textTopOfPage = margin + 86;
var textHeight = height - textTopOfPage - margin - 20;
// Positioned relative to the layout rectangle, which is what an obstacle is measured
// in - so the page coordinates the quote is drawn at are these plus the block's corner.
var quoteInBlock = new XRect(140, 150, measure - 280, 108);
var quote = new XRect(margin + quoteInBlock.X, textTopOfPage + quoteInBlock.Y,
quoteInBlock.Width, quoteInBlock.Height);
// Set on a tint, at a slight slant. ObliqueAngle skews the glyphs where a real
// italic would redraw them, which is the honest tool for display type that has no
// italic of its own to reach for.
gfx.DrawRectangle(new XSolidBrush(XColor.FromArgb(255, 248, 232)), quote);
gfx.DrawLine(new XPen(XColors.DarkSlateGray, 2), quote.X, quote.Y,
quote.X, quote.Y + quote.Height);
formatter.Alignment = XParagraphAlignment.Justify;
formatter.Columns = 2;
formatter.ColumnGap = 18;
// The padding is the obstacle's own, because how much air a thing wants around it is a
// fact about that thing rather than about the text.
formatter.Obstacles.Add(new RectangleObstacle(quoteInBlock, padding: 14));
// Enough copy to fill both columns, because a column left empty would show nothing about
// an obstacle standing in it.
formatter.DrawString(
string.Concat(Enumerable.Repeat(Body, 17)), body,
XBrushes.Black, new XRect(margin, textTopOfPage, measure, textHeight));
formatter.Obstacles.Clear();
formatter.Columns = 1;
formatter.Alignment = XParagraphAlignment.Left;
// Drawn a line at a time with XGraphics rather than flowed, because the slant lives
// on XStringFormat and XTextFormatter's own DrawString does not take one.
var slanted = new XStringFormat
{
ObliqueAngle = 8,
LineAlignment = XLineAlignment.Near
};
// Set to the width of the quote rather than of the page: it straddles the gutter and is
// narrower than either, so the lines are broken to suit it.
var quoteFont = new XFont(Serif, 14);
string[] quoteLines =
{
"“Nothing decides",
"that break except a",
"loop that adds one",
"word at a time.”"
};
for (var line = 0; line < quoteLines.Length; line++)
{
gfx.DrawString(quoteLines[line], quoteFont, XBrushes.DarkSlateGray,
new XRect(quote.X + 18, quote.Y + 16 + line * 21, quote.Width - 32, 21),
slanted);
}
gfx.DrawString("Set with ObliqueAngle, on a tint", new XFont(Sans, 7),
XBrushes.Gray, new XPoint(quote.X + 20, quote.Y + quote.Height + 14));
RunningFoot(gfx, "2");
The full SideWrap demo
const string Prose =
"This paragraph is not split, measured or placed. It is one AddParagraph call, and the "
+ "frame beside it is one AddTextFrame. The renderer subtracts the frame from the area "
+ "the text is laid out in and breaks each line to whatever room is left on the line's "
+ "own band, so the lines level with the frame are short and the lines above and below "
+ "it run the full measure. Nothing here counts characters, probes a rectangle or adds "
+ "one word at a time until the answer stops fitting. ";
var document = new Document
{
Info =
{
Title = "Side wrap"
}
};
document.Styles["Normal"].Font.Name = "Liberation Serif";
document.Styles["Normal"].Font.Size = 10;
document.Styles["Normal"].ParagraphFormat.SpaceAfter = Unit.FromPoint(6);
var caption = document.Styles.AddStyle("Caption", "Normal");
caption.Font.Name = "Liberation Sans";
caption.Font.Size = 8;
caption.Font.Color = Colors.DimGray;
// The style names the side the TEXT runs down, and the shape is put on the other one.
//
// Largest and Both stand the frame away from both margins, and deliberately not in the
// middle: a frame with equal room either side demonstrates nothing, because whichever
// side the text takes looks like the right answer. Set 1.2cm from the left margin, the
// room on its right is nearly four times the room on its left, so a page that puts the
// text down the left is visibly wrong rather than merely different.
(WrapStyle Style, ShapePosition? Where, string Title, string Note)[] pages =
{
(WrapStyle.Right, ShapePosition.Left, "WrapStyle.Right",
"The frame is at the left margin and the text runs down its right - the style names "
+ "the side the text occupies, not the side the shape sits on."),
(WrapStyle.Left, ShapePosition.Right, "WrapStyle.Left",
"The mirror of the page before. Read the two together: if the names were the other "
+ "way round, each page would still look deliberate."),
(WrapStyle.Largest, null, "WrapStyle.Largest",
"The frame stands 1.2cm in from the left margin, so there is nearly four times as "
+ "much room to its right. Each line takes the roomier side, which is why the copy "
+ "runs down the right of it."),
(WrapStyle.Both, null, "WrapStyle.Both",
"The same arrangement asking for either side rather than the roomier one. A line is "
+ "given one span rather than every span, so this lays out as Largest does today; "
+ "the two are kept apart because they say different things and would part company "
+ "if that changed.")
};
foreach ((var style, var where, var title, var note) in pages)
{
var section = document.AddSection();
section.PageSetup.PageFormat = PageFormat.A5;
section.PageSetup.TopMargin = Unit.FromCentimeter(2);
section.PageSetup.BottomMargin = Unit.FromCentimeter(2);
section.PageSetup.LeftMargin = Unit.FromCentimeter(2);
section.PageSetup.RightMargin = Unit.FromCentimeter(2);
var heading = section.AddParagraph(title);
heading.Format.Font.Name = "Liberation Sans";
heading.Format.Font.Bold = true;
heading.Format.Font.Size = 13;
heading.Format.SpaceAfter = Unit.FromPoint(2);
var explanation = section.AddParagraph(note);
explanation.Style = "Caption";
explanation.Format.SpaceAfter = Unit.FromPoint(14);
// The frame is added to the flow like any other element. RelativeVertical.Paragraph is
// what makes it float at all: a shape anchored to the page or the margin is placed
// absolutely and the text is laid out as though it were not there.
var frame = section.AddTextFrame();
frame.Width = Unit.FromCentimeter(4.5);
frame.Height = Unit.FromCentimeter(4);
frame.RelativeVertical = RelativeVertical.Paragraph;
frame.RelativeHorizontal = RelativeHorizontal.Margin;
if (where.HasValue)
frame.Left = where.Value;
else
frame.Left = Unit.FromCentimeter(1.2);
frame.FillFormat.Color = new Color(246, 243, 234);
frame.LineFormat.Width = 0.75;
frame.LineFormat.Color = Colors.DarkSlateGray;
frame.MarginTop = Unit.FromPoint(8);
frame.MarginLeft = Unit.FromPoint(10);
frame.MarginRight = Unit.FromPoint(10);
frame.WrapFormat.Style = style;
// All four distances mean something for a side-wrapped shape. Left and Right hold the
// text off horizontally, as they always claimed to; Top and Bottom grow the obstacle
// vertically, so a line whose box would otherwise clear the frame by a hair is pushed
// past it instead of grazing it.
frame.WrapFormat.DistanceLeft = Unit.FromPoint(10);
frame.WrapFormat.DistanceRight = Unit.FromPoint(10);
frame.WrapFormat.DistanceTop = Unit.FromPoint(4);
frame.WrapFormat.DistanceBottom = Unit.FromPoint(4);
var inside = frame.AddParagraph("A sidebar");
inside.Format.Font.Name = "Liberation Sans";
inside.Format.Font.Bold = true;
inside.Format.SpaceAfter = Unit.FromPoint(4);
var insideBody = frame.AddParagraph(
"Whatever goes in the frame is laid out inside it, independently of the copy "
+ "flowing past outside.");
insideBody.Format.Font.Size = 8.5;
var body = section.AddParagraph(string.Concat(Prose, Prose, Prose));
body.Format.Alignment = ParagraphAlignment.Justify;
body.Format.FirstLineIndent = Unit.FromPoint(0);
}
var renderer = new PdfDocumentRenderer(unicode: true) { Document = document };
renderer.RenderDocument();
return renderer.PdfDocument;