Draw with your own renderer
DocWright's PDF and image renderers both draw one LayoutDocument. A renderer of your own, such as a canvas, an SVG writer or an editor surface, can draw the same one. DocWrightConverter.Layout hands it the pages Convert would render, with the font engine they were measured with.
Lay out, then draw
Layout runs Convert's pipeline up to the point where rendering begins: options, the font-engine cache, the fonts the document embeds, resolution, the font preflight, layout and the resource limits. It returns a LaidOutConversion with three values:
| Property | What it is |
|---|---|
Document |
The LayoutDocument: every page, with its glyph runs, rules, images and links in twips. |
Fonts |
The FontEngine layout measured with, the document's embedded fonts included. |
RenderOptions |
The page selection, strict mode and limits a conversion passes its renderer. |
For each glyph run, FontEngine.ResolveRun gives the face the built-in renderers draw it with, and GlyphRunPlanner.Plan gives its glyphs:
using DocWright;
using DocWright.Core.Diagnostics;
using DocWright.Core.Rendering;
using DocWright.Fonts;
// Resolve and lay out exactly as Convert would, without rendering.
LaidOutConversion laidOut = converter.Layout(document);
foreach (LayoutPage page in laidOut.Document.Pages)
{
foreach (ILayoutPrimitive primitive in page.Primitives)
{
if (primitive is not GlyphRun run)
{
continue;
}
// The face the built-in renderers draw this run with, from the engine layout measured with.
FontFace face = laidOut.Fonts.ResolveRun(run);
PlannedGlyphCluster[] clusters = GlyphRunPlanner.Plan(run, face, NullConversionDiagnostics.Instance);
Console.WriteLine($"\"{run.Text}\" -> {face.PostScriptName}, {clusters.Length} clusters");
}
}
Output
"Plain," -> TimesNewRomanPSMT, 6 clusters
" " -> TimesNewRomanPSMT, 1 clusters
"bold," -> TimesNewRomanPS-BoldMT, 5 clusters
" " -> TimesNewRomanPS-BoldMT, 1 clusters
"bold" -> TimesNewRomanPS-BoldItalicMT, 4 clusters
" " -> TimesNewRomanPS-BoldItalicMT, 1 clusters
"italic." -> TimesNewRomanPS-BoldItalicMT, 7 clusters
identical to Convert: True
The same bytes as Convert
Hand the three values to a built-in renderer and it writes what Convert writes. That is how you know your renderer starts from the right layout:
using DocWright;
using DocWright.Core.Diagnostics;
using DocWright.Renderers.Pdf;
// The same three values, handed to a built-in renderer, write the PDF Convert writes.
using var viaLayout = new MemoryStream();
new PdfLayoutRenderer(laidOut.Fonts).Render(
laidOut.Document, viaLayout, laidOut.RenderOptions, NullConversionDiagnostics.Instance);
using var viaConvert = new MemoryStream();
converter.Convert(document, viaConvert);
bool identical = viaLayout.ToArray().AsSpan().SequenceEqual(viaConvert.ToArray());
Console.WriteLine($"identical to Convert: {identical}");
Map a point back to the document
Set ConvertOptions.EmitSourceReferences and every glyph run of body text says which document text it came from (GlyphRun.Source). That is what hit-testing, search highlighting or a caret needs:
using DocWright;
using DocWright.Core.Rendering;
using DocWright.Dom;
using DocWright.Dom.Editing;
var converter = new DocWrightConverter();
using var document = new WordDocument();
Paragraph paragraph = document.AppendParagraph("Click on a word ");
paragraph.AppendText("in this sentence.", new CharacterFormat { Bold = true });
LaidOutConversion laidOut = converter.Layout(document, new ConvertOptions { EmitSourceReferences = true });
// A point on page 1, in twips: on the word "this".
GlyphRun target = laidOut.Document.Pages[0].Primitives.OfType<GlyphRun>().First(r => r.Text == "this");
int x = target.BaselineX.Value + 200, y = target.BaselineY.Value - 100;
foreach (GlyphRun run in laidOut.Document.Pages[0].Primitives.OfType<GlyphRun>())
{
int left = run.BaselineX.Value;
int right = left + run.ClusterAdvances.Sum(c => c.Advance.Value);
if (x < left || x >= right || y > run.BaselineY.Value || y < run.BaselineY.Value - run.FontSize.Value)
{
continue;
}
// The cluster under the point, then the segment that holds it.
int pen = left, cluster = 0;
foreach (GlyphClusterAdvance advance in run.ClusterAdvances)
{
if (x < pen + advance.Advance.Value) { cluster = advance.Cluster; break; }
pen += advance.Advance.Value;
}
GlyphRunSourceSegment segment = run.Source.Segments.First(
s => cluster >= s.TextOffset && cluster < s.TextOffset + s.TextLength);
var node = (TextRun)document.FindNode(DomNodeId.FromValue(segment.NodeKey))!;
int offset = segment.Start + (cluster - segment.TextOffset);
Console.WriteLine($"run \"{run.Text}\" -> node {node.NodeId}, \"{node.Text}\"");
Console.WriteLine($"character {offset}: '{node.Text[offset]}'");
ctx.Check(node.Text[offset] == 'i', "the point maps to the character under it");
}
Output
run "this" -> node n2, "in this sentence."
character 5: 'i'
- Each segment says that the run's characters
[TextOffset, TextOffset + TextLength)came from the node's[Start, Start + Length).NodeKeyis the node'sDomNodeId.Value; look it up withWordDocument.FindNodein the document you laid out. - One glyph run can hold two document runs where ligatures or kerning join runs of identical formatting, one segment each. A run cut across lines or pages names each character once.
- Offsets are the document's characters: caps change only the case drawn, and a hyphen drawn at a break is a segment of zero length.
- Text layout produces is
Generated:GeneratedKindsays what made it (ListLabel,FieldResult,NoteReferenceMark,NoteNumberMark,ParagraphMark,ChartText,PictureText,TabLeader) andOwnerKeynames the node it belongs to. A field's cached result isText, pointing at the runs it was read from. With the option on, no run isNone. - Nothing drawn changes. Renderers ignore sources, so the PDF is the same bytes with the option on or off. It is off by default.
What to know
- Draw with
Fonts, not an engine of your own. Every line was broken with that engine's advances. Anew FontEngine(), or a pipeline you build yourself fromStyleResolverandLayoutEngine, doesn't see the document's embedded fonts, so its faces can differ. - A run's
FontFamilyis the family layout settled on, after substitution. A run in a font the machine lacks arrives as the substitute, andResolveRunreturns that family's real bold or italic face. - Tagged PDF structure follows
ConvertOptions.TaggedPdf, as inConvert.ConvertToImagesalways lays out without it, so leaveTaggedPdfoff (the default) to draw whatConvertToImagesdraws. - The whole document is laid out.
ConvertOptions.Pagesreaches your renderer asRenderOptions.Pages.Layoutitself enforcesResourceLimits.MaxPages, and underStrictModeit refuses whatConvertrefuses. - PDF metadata belongs to the renderer. If you set
ConvertOptions.Metadataand draw withPdfLayoutRendereryourself, pass aPdfRenderMetadatato its constructor.