Table of Contents

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). NodeKey is the node's DomNodeId.Value; look it up with WordDocument.FindNode in 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: GeneratedKind says what made it (ListLabel, FieldResult, NoteReferenceMark, NoteNumberMark, ParagraphMark, ChartText, PictureText, TabLeader) and OwnerKey names the node it belongs to. A field's cached result is Text, pointing at the runs it was read from. With the option on, no run is None.
  • 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. A new FontEngine(), or a pipeline you build yourself from StyleResolver and LayoutEngine, doesn't see the document's embedded fonts, so its faces can differ.
  • A run's FontFamily is the family layout settled on, after substitution. A run in a font the machine lacks arrives as the substitute, and ResolveRun returns that family's real bold or italic face.
  • Tagged PDF structure follows ConvertOptions.TaggedPdf, as in Convert. ConvertToImages always lays out without it, so leave TaggedPdf off (the default) to draw what ConvertToImages draws.
  • The whole document is laid out. ConvertOptions.Pages reaches your renderer as RenderOptions.Pages. Layout itself enforces ResourceLimits.MaxPages, and under StrictMode it refuses what Convert refuses.
  • PDF metadata belongs to the renderer. If you set ConvertOptions.Metadata and draw with PdfLayoutRenderer yourself, pass a PdfRenderMetadata to its constructor.