Diagnostics
Papyra reports problems at build time through two separate channels. They use different number ranges, so a code tells you which one it came from.
| Range | Channel | Raised by |
|---|---|---|
PAPY0xx |
MSBuild task | License validation - see licensing |
PAPY1xx |
Roslyn analyzer | API misuse the compiler can prove from your source |
PAPY2xx |
Layout check | Problems only visible once the document has been measured - shown in the companion app |
The hundreds digit tells you when a problem becomes visible, and how certain the statement is. The analyzer ships inside the Papyra NuGet package; nothing extra to install.
What the analyzer will and will not tell you
Section titled “What the analyzer will and will not tell you”Every rule below is deliberately conservative: it reports only what is provable from the code in front of it - literal arguments and inline initializers. Options that travel through a variable, a field, or a method call are invisible to it, and the runtime validation stays authoritative. That trade is intentional. An analyzer that guesses produces false warnings, and a rule that cries wolf gets switched off.
So a clean build does not prove a document renders. It proves you did not make one of the specific mistakes listed here.
Roslyn diagnostics
Section titled “Roslyn diagnostics”PAPY101 - PDF attachments require PdfStandard.PdfA3 {#papy101}
Section titled “PAPY101 - PDF attachments require PdfStandard.PdfA3 {#papy101}”Severity: Warning
Setting Attachments requires Standard = PdfStandard.PdfA3 or PdfStandard.Pdf20; setting FacturX requires exactly PdfStandard.PdfA3 (its XMP extension schema needs the PDF/A machinery). Any other combination throws ArgumentException when you call Render - PDF/A-2 (the default) and PDF/A-4 forbid arbitrary embedded files.
// Warns: Attachments without PDF/A-3var options = new PdfRenderOptions{ Attachments = [new PdfAttachment("data.xml", bytes, "text/xml")],};
// Correctvar options = new PdfRenderOptions{ Standard = PdfStandard.PdfA3, Attachments = [new PdfAttachment("data.xml", bytes, "text/xml")],};The rule fires on object-creation initializers, where an absent Standard provably means the PDF/A-2 default. On a with expression it only fires when that initializer explicitly assigns a wrong standard - the operand it copies from may already carry PDF/A-3, and the analyzer will not assume otherwise.
See e-invoicing for the Factur-X case and PDF render options for the full option surface.
PAPY102 - Watermark options are contradictory {#papy102}
Section titled “PAPY102 - Watermark options are contradictory {#papy102}”Severity: Warning
A watermark carries either text or an image - never both, never neither - and Color, Rotation, FontSize and FontFamily only mean something for text. All three mistakes throw from Build(), long after the call that caused them.
doc.Watermark(w => w.Text("DRAFT").Image(logo)); // warns: pick onedoc.Watermark(w => w.Opacity(0.2f)); // warns: needs text or an imagedoc.Watermark(w => w.Image(logo).Rotation(30f)); // warns: Rotation is text-onlyOpacity is the exception: it applies to both kinds and never triggers this rule.
The options have to be visible in the same expression to be provable, so a configure delegate passed as a variable or method group is left to the runtime guard.
PAPY103 - Call modifies a block that is not there {#papy103}
Section titled “PAPY103 - Call modifies a block that is not there {#papy103}”Severity: Warning
KeepTogether(), KeepWithNext() and Heading() attach to the block that precedes them. They throw when nothing precedes them, and when what precedes them is a page break or a table of contents - neither of which carries those flags.
doc.KeepTogether(); // warns: nothing to keep togetherdoc.PageBreak().KeepWithNext(); // warns: cannot apply to a page breakdoc.Table(t => t.Columns(2)).Heading(2); // warns: needs an explicit titleThe last one is a subtler rule: a bare Heading(level) takes its title from the preceding paragraph’s own text, so anything that is not a paragraph needs Heading(2, "Figures").
Because the preceding block does not have to sit in the same expression, the rule reports only what it can prove: either the chain itself contains the preceding block, or nothing was appended anywhere earlier in the same lambda. A block added in an earlier statement silences it.
PAPY104 - Argument is outside the range Papyra accepts {#papy104}
Section titled “PAPY104 - Argument is outside the range Papyra accepts {#papy104}”Severity: Warning
The builders validate their arguments and throw at compose time. When the argument is a literal, the mistake is visible while you write it:
doc.Watermark(w => w.Text("DRAFT").Opacity(1.5f)); // warns: at most 1doc.MinRowHeight(0f); // warns: must be greater than zerodoc.Paragraph(p => p.Text("Intro")).Heading(7, "X"); // warns: levels run 1 to 6doc.Line(alignment: TextAlignment.Justify); // warns: lines cannot be justifiedCovered: watermark opacity and font size, the minimum row height, heading and table-of-contents levels, indent widths, line widths, column counts, column and row spans, item weights and widths, list start values, column definitions, and Color.WithAlpha. Plus the two places that reject TextAlignment.Justify - lines and images. Justified text stays perfectly legal; the rule is tied to those specific parameters, not to the enum member.
The rule only judges compile-time constants. A value that arrives through a variable, a field, or a method call is left alone, and an optional argument you never wrote is never reported.
PAPY105 - Font family was never registered {#papy105}
Section titled “PAPY105 - Font family was never registered {#papy105}”Severity: Suggestion
Font resolution never fails. A family Papyra does not know falls back to a built-in face, so the document just comes out in the wrong typeface - and only in PDF, because DOCX, XLSX and HTML pass the name through to the viewer. The same FontFamily string can therefore render two different ways.
doc.RegisterFont("Inter", interBytes) .Paragraph(p => p.Text("Title", new TextStyle(FontFamily: "Intr"))); // suggests: typoTo keep this quiet in the cases where a fallback is the point, the rule only speaks up in documents that register fonts at all - that is what makes an unknown name a likely typo rather than a deliberate system font. It also stays silent for the families that are meant to resolve to a built-in: Arial, Helvetica, Times New Roman, Georgia, Courier New, Consolas and the rest of the aliases, since Liberation Sans, Serif and Mono exist precisely to stand in for them.
Checked wherever a family name is a literal: the TextStyle constructor, WithFontFamily(...), a with expression, and WatermarkBuilder.FontFamily(...).
PAPY106 - PDF encryption requires PdfStandard.Pdf20 {#papy106}
Section titled “PAPY106 - PDF encryption requires PdfStandard.Pdf20 {#papy106}”Severity: Warning
The PDF/A standards forbid encryption, so only plain PDF 2.0 output can be encrypted. Setting Encryption while Standard is a PDF/A value (PDF/A-2 is the default) throws ArgumentException when you call RenderAsPdf.
// Warns: Encryption without Pdf20var options = new PdfRenderOptions{ Encryption = new PdfEncryption { UserPassword = "secret" },};
// Correctvar options = new PdfRenderOptions{ Standard = PdfStandard.Pdf20, Encryption = new PdfEncryption { UserPassword = "secret" },};The rule fires on object-creation initializers, where an absent Standard provably means the PDF/A-2 default. On a with expression it only fires when that initializer explicitly assigns a wrong standard - the operand it copies from may already carry Pdf20, and the analyzer will not assume otherwise.
See the encryption guide for passwords and permissions.
Layout diagnostics
Section titled “Layout diagnostics”These come from a different place than everything above, and the difference matters more than it looks.
PAPY1xx says a call is wrong: the compiler can prove it from the source, and it is wrong no matter what data you feed the document. PAPY2xx says this document - with this text, this font, this page size - comes out badly. That can only be known after the text has been measured and the pages have been broken, which is why these appear in the companion preview rather than in your build.
The consequence is worth stating plainly: a clean build does not mean a good-looking document, and a clean preview does not mean correct code. The two surfaces answer different questions.
These are reported by the companion app as red overlays, badges in the element tree, and entries in the issues panel.
PAPY201 - Text overflows its container {#papy201}
Section titled “PAPY201 - Text overflows its container {#papy201}”A single unbreakable word is wider than the space it has. Wrapped text never triggers this: the layout engine breaks at whitespace, so only a word with no break opportunity - a long identifier, a URL, an unspaced code - can stick out.
Widen the column, reduce the font size, or introduce a break opportunity in the text.
PAPY202 - Content is wider than the space available {#papy202}
Section titled “PAPY202 - Content is wider than the space available {#papy202}”Reported in three situations: a table’s fixed columns add up to more than the table’s own width, a row’s fixed items add up to more than the content width, or a laid-out element simply extends past the right edge.
For a top-level table the available width is the page content area; for a nested one it is the host cell’s inner width, which is usually the surprise.
PAPY203 - Element is taller than one page {#papy203}
Section titled “PAPY203 - Element is taller than one page {#papy203}”The element cannot fit on a page even by itself, so it will be split or clipped wherever it lands. Note the available height is the page minus margins, minus the header and footer bands, and minus whatever footnotes claimed on that particular page - a tall header or a long note eats into it, so the figure is per page rather than document-wide.
PAPY204 - Element is clipped by a page break {#papy204}
Section titled “PAPY204 - Element is clipped by a page break {#papy204}”The element starts on one page and ends on the next, at a boundary that cuts it visually. KeepTogether() is the usual answer, keeping in mind that a block taller than a page still has to split.
Code fixes
Section titled “Code fixes”Three rules ship a fix:
- PAPY101 - Set Standard = PdfStandard.PdfA3. Writes the assignment into the offending initializer, replacing a wrong standard or adding a missing one.
- PAPY102 - Remove the text-only watermark options. Drops
Color/Rotation/FontSize/FontFamilyfrom an image watermark’s chain, leavingOpacityalone since it applies to both kinds. - PAPY106 - Set Standard = PdfStandard.Pdf20. Writes the assignment into the offending initializer, replacing a wrong standard or adding a missing one.
Fixes are offered only where exactly one answer is correct. A watermark that sets both Text and Image gets none: either could be the one you meant, and picking for you would be guessing at intent.
Suppressing a diagnostic
Section titled “Suppressing a diagnostic”Prefer fixing the code. Where you genuinely know better - most often because the value the analyzer cannot see is correct - suppress narrowly:
#pragma warning disable PAPY101 // Standard is set on the options passed inOr project-wide in .editorconfig, which is the right place to downgrade a rule you do not want as a warning:
[*.cs]dotnet_diagnostic.PAPY101.severity = suggestionLicense build errors
Section titled “License build errors”The PAPY0xx codes come from the MSBuild task, not the analyzer. They fail a Release build; in a Debug build they are downgraded to a warning instead. They are documented with the licensing model in licensing.
© 2026 Stefan Nikolei. Payments are processed by Paddle.com as merchant of record.