Repository: QuestPDF/library
Branch: main
Commit: 83195ce94769
Files: 479
Total size: 1.8 MB
Directory structure:
gitextract_ymraibqd/
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── companion_app_feedback.md
│ │ └── feature_request.md
│ ├── SECURITY.md
│ └── workflows/
│ └── main.yml
├── .gitignore
├── LICENSE.md
├── README.md
└── Source/
├── .config/
│ ├── dotnet-tools.json
│ └── stryker-config.json
├── .editorconfig
├── QuestPDF/
│ ├── Build/
│ │ ├── QuestPDF.targets
│ │ └── net4/
│ │ └── QuestPDF.targets
│ ├── Companion/
│ │ ├── CompanionExtensions.cs
│ │ ├── CompanionModels.cs
│ │ ├── CompanionService.cs
│ │ ├── Helpers.cs
│ │ ├── HotReloadManager.cs
│ │ └── Previewer.cs
│ ├── Drawing/
│ │ ├── DocumentCanvases/
│ │ │ ├── CompanionDocumentCanvas.cs
│ │ │ ├── DiscardDocumentCanvas.cs
│ │ │ ├── ImageDocumentCanvas.cs
│ │ │ ├── PdfDocumentCanvas.cs
│ │ │ ├── SemanticDocumentCanvas.cs
│ │ │ ├── SvgDocumentCanvas.cs
│ │ │ └── XpsDocumentCanvas.cs
│ │ ├── DocumentContainer.cs
│ │ ├── DocumentGenerator.cs
│ │ ├── DocumentPageSnapshot.cs
│ │ ├── DrawingCanvases/
│ │ │ ├── DiscardDrawingCanvas.cs
│ │ │ ├── ProxyDrawingCanvas.cs
│ │ │ ├── SemanticDrawingCanvas.cs
│ │ │ └── SkiaDrawingCanvas.cs
│ │ ├── Exceptions/
│ │ │ ├── DocumentComposeException.cs
│ │ │ ├── DocumentDrawingException.cs
│ │ │ ├── DocumentLayoutException.cs
│ │ │ └── InitializationException.cs
│ │ ├── FontManager.cs
│ │ ├── Proxy/
│ │ │ ├── ElementProxy.cs
│ │ │ ├── LayoutDebugging.cs
│ │ │ ├── LayoutOverflowVisualization.cs
│ │ │ ├── LayoutProxy.cs
│ │ │ ├── OverflowDebuggingProxy.cs
│ │ │ ├── SnapshotCacheRecorderProxy.cs
│ │ │ └── TreeTraversal.cs
│ │ ├── SemanticTreeManager.cs
│ │ ├── SpacePlan.cs
│ │ └── SpacePlanType.cs
│ ├── Elements/
│ │ ├── Alignment.cs
│ │ ├── ArtifactTag.cs
│ │ ├── AspectRatio.cs
│ │ ├── Column.cs
│ │ ├── Constrained.cs
│ │ ├── Container.cs
│ │ ├── ContentDirectionSetter.cs
│ │ ├── DebugArea.cs
│ │ ├── DebugPointer.cs
│ │ ├── Decoration.cs
│ │ ├── DefaultTextStyle.cs
│ │ ├── Dynamic.cs
│ │ ├── DynamicImage.cs
│ │ ├── DynamicSvgImage.cs
│ │ ├── ElementPositionLocator.cs
│ │ ├── Empty.cs
│ │ ├── EnsureSpace.cs
│ │ ├── Extend.cs
│ │ ├── Grid.cs
│ │ ├── Hyperlink.cs
│ │ ├── Image.cs
│ │ ├── Inlined.cs
│ │ ├── Layers.cs
│ │ ├── Lazy.cs
│ │ ├── Line.cs
│ │ ├── MultiColumn.cs
│ │ ├── Padding.cs
│ │ ├── Page.cs
│ │ ├── PageBreak.cs
│ │ ├── Placeholder.cs
│ │ ├── PreventPageBreak.cs
│ │ ├── RepeatContent.cs
│ │ ├── Rotate.cs
│ │ ├── Row.cs
│ │ ├── Scale.cs
│ │ ├── ScaleToFit.cs
│ │ ├── Section.cs
│ │ ├── SectionLink.cs
│ │ ├── SemanticTag.cs
│ │ ├── ShowEntire.cs
│ │ ├── ShowIf.cs
│ │ ├── ShowOnce.cs
│ │ ├── Shrink.cs
│ │ ├── SimpleRotate.cs
│ │ ├── SkipOnce.cs
│ │ ├── SourceCodePointer.cs
│ │ ├── StopPaging.cs
│ │ ├── StyledBox.cs
│ │ ├── SvgImage.cs
│ │ ├── SvgPath.cs
│ │ ├── Table/
│ │ │ ├── DynamicDictionary.cs
│ │ │ ├── ITableCellContainer.cs
│ │ │ ├── Table.cs
│ │ │ ├── TableCell.cs
│ │ │ ├── TableCellRenderingCommand.cs
│ │ │ ├── TableColumnDefinition.cs
│ │ │ ├── TableLayoutPlanner.cs
│ │ │ └── TableLayoutValidator.cs
│ │ ├── Text/
│ │ │ ├── Items/
│ │ │ │ ├── ITextBlockItem.cs
│ │ │ │ ├── TextBlockElement.cs
│ │ │ │ ├── TextBlockHyperlink.cs
│ │ │ │ ├── TextBlockPageNumber.cs
│ │ │ │ ├── TextBlockParagraphSpacing.cs
│ │ │ │ ├── TextBlockSectionLink.cs
│ │ │ │ └── TextBlockSpan.cs
│ │ │ ├── SkParagraphBuilderPoolManager.cs
│ │ │ └── TextBlock.cs
│ │ ├── Translate.cs
│ │ ├── Unconstrained.cs
│ │ └── ZIndex.cs
│ ├── Fluent/
│ │ ├── AlignmentExtensions.cs
│ │ ├── ColumnExtensions.cs
│ │ ├── ComponentExtentions.cs
│ │ ├── ConstrainedExtensions.cs
│ │ ├── ContentDirectionExtensions.cs
│ │ ├── DebugExtensions.cs
│ │ ├── DecorationExtensions.cs
│ │ ├── DocumentOperation.cs
│ │ ├── DynamicComponentExtensions.cs
│ │ ├── ElementExtensions.cs
│ │ ├── ExtendExtensions.cs
│ │ ├── GenerateExtensions.cs
│ │ ├── GridExtensions.cs
│ │ ├── ImageExtensions.cs
│ │ ├── InlinedExtensions.cs
│ │ ├── LayerExtensions.cs
│ │ ├── LineExtensions.cs
│ │ ├── MinimalApi.cs
│ │ ├── MultiColumnExtensions.cs
│ │ ├── PaddingExtensions.cs
│ │ ├── PageExtensions.cs
│ │ ├── RotateExtensions.cs
│ │ ├── RowExtensions.cs
│ │ ├── ScaleExtensions.cs
│ │ ├── SemanticExtensions.cs
│ │ ├── ShrinkExtensions.cs
│ │ ├── StyledBoxExtensions.cs
│ │ ├── SvgExtensions.cs
│ │ ├── TableExtensions.cs
│ │ ├── TextExtensions.cs
│ │ ├── TextSpanDescriptorExtensions.cs
│ │ ├── TextStyleExtensions.cs
│ │ └── TranslateExtensions.cs
│ ├── Helpers/
│ │ ├── CallerArgumentExpression.cs
│ │ ├── ColorParser.cs
│ │ ├── Colors.cs
│ │ ├── FontFeatures.cs
│ │ ├── Fonts.cs
│ │ ├── Helpers.cs
│ │ ├── IsExternalInit.cs
│ │ ├── LicenseChecker.cs
│ │ ├── NativeDependencyCompatibilityChecker.cs
│ │ ├── NativeDependencyProvider.cs
│ │ ├── PageSizes.cs
│ │ ├── Placeholders.cs
│ │ └── TemporaryStorage.cs
│ ├── Infrastructure/
│ │ ├── AspectRatioOption.cs
│ │ ├── BoxShadowStyle.cs
│ │ ├── Color.cs
│ │ ├── ContainerElement.cs
│ │ ├── ContentDirection.cs
│ │ ├── DocumentMetadata.cs
│ │ ├── DocumentSettings.cs
│ │ ├── Element.cs
│ │ ├── EmptyContainer.cs
│ │ ├── FontPosition.cs
│ │ ├── FontWeight.cs
│ │ ├── HorizontalAlignment.cs
│ │ ├── IComponent.cs
│ │ ├── IContainer.cs
│ │ ├── IContentDirectionAware.cs
│ │ ├── IDocument.cs
│ │ ├── IDocumentCanvas.cs
│ │ ├── IDocumentContainer.cs
│ │ ├── IDrawingCanvas.cs
│ │ ├── IDynamicComponent.cs
│ │ ├── IElement.cs
│ │ ├── IMergedDocument.cs
│ │ ├── IPageContext.cs
│ │ ├── ISemanticAware.cs
│ │ ├── IStateful.cs
│ │ ├── Image.cs
│ │ ├── ImageCompressionQuality.cs
│ │ ├── ImageFormat.cs
│ │ ├── ImageGenerationSettings.cs
│ │ ├── ImageScaling.cs
│ │ ├── ImageSize.cs
│ │ ├── LicenseType.cs
│ │ ├── PageContext.cs
│ │ ├── Position.cs
│ │ ├── Size.cs
│ │ ├── SourceCodePath.cs
│ │ ├── StaticImageCache.cs
│ │ ├── SvgImage.cs
│ │ ├── TextDirection.cs
│ │ ├── TextHorizontalAlignment.cs
│ │ ├── TextInjectedElementAlignment.cs
│ │ ├── TextStyle.cs
│ │ ├── TextStyleManager.cs
│ │ ├── Unit.cs
│ │ └── VerticalAlignment.cs
│ ├── LatoFont/
│ │ └── OFL.txt
│ ├── NugetStrongNameSigningKeyForQuestPDF.snk
│ ├── Qpdf/
│ │ ├── JobConfiguration.cs
│ │ ├── MimeHelper.cs
│ │ ├── QpdfAPI.cs
│ │ ├── QpdfNativeDependencyCompatibilityChecker.cs
│ │ └── SimpleJsonSerializer.cs
│ ├── QuestPDF.csproj
│ ├── Resources/
│ │ ├── Contributors.md
│ │ ├── Description.md
│ │ ├── Documentation.xml
│ │ ├── ExternalDependencyLicenses/
│ │ │ ├── emsdk.txt
│ │ │ ├── expat.txt
│ │ │ ├── harfbuzz.txt
│ │ │ ├── libgrapheme.txt
│ │ │ ├── libjpeg-turbo.txt
│ │ │ ├── libpng.txt
│ │ │ ├── libwebp.txt
│ │ │ ├── ninja-build.txt
│ │ │ ├── qpdf.txt
│ │ │ ├── readme.txt
│ │ │ ├── skia.txt
│ │ │ ├── wuffs.txt
│ │ │ └── zlib.txt
│ │ ├── LatinWords.txt
│ │ ├── MimeTypes.csv
│ │ ├── PackageLicense.md
│ │ └── ReleaseNotes.txt
│ ├── Settings.cs
│ └── Skia/
│ ├── SkBitmap.cs
│ ├── SkBoxShadow.cs
│ ├── SkCanvas.cs
│ ├── SkCanvasMatrix.cs
│ ├── SkData.cs
│ ├── SkDateTime.cs
│ ├── SkDocument.cs
│ ├── SkImage.cs
│ ├── SkNativeDependencyCompatibilityChecker.cs
│ ├── SkPaint.cs
│ ├── SkPdfDocument.cs
│ ├── SkPdfTag.cs
│ ├── SkPicture.cs
│ ├── SkPictureRecorder.cs
│ ├── SkPoint.cs
│ ├── SkRect.cs
│ ├── SkResourceProvider.cs
│ ├── SkRoundedRect.cs
│ ├── SkSemanticNodeSpecialId.cs
│ ├── SkSize.cs
│ ├── SkSvgCanvas.cs
│ ├── SkSvgImage.cs
│ ├── SkText.cs
│ ├── SkWriteStream.cs
│ ├── SkXpsDocument.cs
│ ├── SkiaAPI.cs
│ ├── Text/
│ │ ├── SkFontCollection.cs
│ │ ├── SkFontManager.cs
│ │ ├── SkParagraph.cs
│ │ ├── SkParagraphBuilder.cs
│ │ ├── SkTextStyle.cs
│ │ ├── SkTypeface.cs
│ │ ├── SkTypefaceProvider.cs
│ │ └── SkUnicode.cs
│ └── Utf8StringMarshaller.cs
├── QuestPDF.Companion.TestRunner/
│ ├── Program.cs
│ └── QuestPDF.Companion.TestRunner.csproj
├── QuestPDF.ConformanceTests/
│ ├── DecorationTests.cs
│ ├── DynamicTests.cs
│ ├── FooterTests.cs
│ ├── HeaderTests.cs
│ ├── HyperlinkInFooterTests.cs
│ ├── HyperlinkTests.cs
│ ├── IgnoreTests.cs
│ ├── ImageTests.cs
│ ├── LazyTests.cs
│ ├── LineTests.cs
│ ├── ListTests.cs
│ ├── MultiColumnTests.cs
│ ├── OrderOfSemanticItemsTests.cs
│ ├── QuestPDF.ConformanceTests.csproj
│ ├── Resources/
│ │ ├── zugferd-factur-x.xml
│ │ └── zugferd-xmp-metadata.xml
│ ├── StyledBoxTests.cs
│ ├── SvgTests.cs
│ ├── Table/
│ │ ├── TableWithFooterTests.cs
│ │ ├── TableWithHeaderCellsSpanningMultipleColumnsTests.cs
│ │ ├── TableWithHeaderCellsSpanningMultipleRowsTests.cs
│ │ ├── TableWithHorizontalHeadersTests.cs
│ │ ├── TableWithVerticalHeadersTests.cs
│ │ └── TableWithoutHeadersTests.cs
│ ├── TableOfContentsTests.cs
│ ├── TestEngine/
│ │ ├── ConformanceTestBase.cs
│ │ ├── MustangConformanceTestRunner.cs
│ │ ├── SemanticAwareDrawingCanvas.cs
│ │ ├── SemanticTreeTestRunner.cs
│ │ └── VeraPdfConformanceTestRunner.cs
│ ├── TestsSetup.cs
│ └── ZugferdTests.cs
├── QuestPDF.DocumentationExamples/
│ ├── AccessibilityExamples.cs
│ ├── AlignmentExamples.cs
│ ├── AspectRatioExamples.cs
│ ├── BackgroundExamples.cs
│ ├── BarcodeExamples.cs
│ ├── BorderExamples.cs
│ ├── ChartExamples.cs
│ ├── CodePatterns/
│ │ ├── CodePatternAddressComponentExample.cs
│ │ ├── CodePatternCapturePositionExample.cs
│ │ ├── CodePatternComponentProgressbarComponentExample.cs
│ │ ├── CodePatternConfigurableComponentExample.cs
│ │ ├── CodePatternContentStylingExample.cs
│ │ ├── CodePatternDocumentStructureExample.cs
│ │ ├── CodePatternDynamicComponentExample.cs
│ │ ├── CodePatternExecutionOrderExample.cs
│ │ ├── CodePatternExtesionMethodExample.cs
│ │ └── CodePatternLocalHelpersExample.cs
│ ├── ColorsExamples.cs
│ ├── ColumnExamples.cs
│ ├── ComplexGraphicsExamples.cs
│ ├── ConstrainedExamples.cs
│ ├── ContentDirectionExamples.cs
│ ├── CustomFirstPageExample.cs
│ ├── DebugAreaExamples.cs
│ ├── DecorationExamples.cs
│ ├── DefaultTextStyleExamples.cs
│ ├── DocumentOperationExamples.cs
│ ├── EnsureSpaceExamples.cs
│ ├── FlipExamples.cs
│ ├── HyperlinkExamples.cs
│ ├── ImageExamples.cs
│ ├── InlinedExamples.cs
│ ├── LayersExamples.cs
│ ├── LazyExamples.cs
│ ├── LicenseSetup.cs
│ ├── LineExamples.cs
│ ├── ListExamples.cs
│ ├── MapExample.cs
│ ├── MergingDocumentsExamples.cs
│ ├── MultiColumnExamples.cs
│ ├── PaddingExamples.cs
│ ├── PageBreakExamples.cs
│ ├── PageExamples.cs
│ ├── PlaceholderExamples.cs
│ ├── PreventPageBreakExamples.cs
│ ├── QuestPDF.DocumentationExamples.csproj
│ ├── RepeatExamples.cs
│ ├── Resources/
│ │ └── semantic-book-content.json
│ ├── RotateExamples.cs
│ ├── RoundedCornersExamples.cs
│ ├── RowExamples.cs
│ ├── ScaleExamples.cs
│ ├── ScaleToFitExamples.cs
│ ├── SectionExamples.cs
│ ├── SemanticExamples.cs
│ ├── ShadowExamples.cs
│ ├── ShowEntireExamples.cs
│ ├── ShowOnceExamples.cs
│ ├── SkiaSharpHelpers.cs
│ ├── SkiaSharpIntegrationExamples.cs
│ ├── SkipOnceExamples.cs
│ ├── StopPagingExamples.cs
│ ├── TableExamples.cs
│ ├── Text/
│ │ ├── ParagraphStyleExamples.cs
│ │ ├── TextBasicExamples.cs
│ │ ├── TextInjectContent.cs
│ │ └── TextStyleExamples.cs
│ ├── TranslateExamples.cs
│ ├── UnconstrainedExamples.cs
│ └── ZIndexExamples.cs
├── QuestPDF.LayoutTests/
│ ├── ColumnTests.cs
│ ├── LineTests.cs
│ ├── MultiColumnTests.cs
│ ├── PaddingTests.cs
│ ├── QuestPDF.LayoutTests.csproj
│ ├── RotateTests.cs
│ ├── RowTests.cs
│ ├── ScaleTests.cs
│ ├── Setup.cs
│ ├── ShowIfTests.cs
│ ├── ShrinkTests.cs
│ ├── SimpleRotateTests.cs
│ ├── StopPagingTests.cs
│ ├── TableTests.cs
│ ├── TestEngine/
│ │ ├── ContinuousBlock.cs
│ │ ├── DrawingRecorder.cs
│ │ ├── ElementObserver.cs
│ │ ├── ElementObserverSetter.cs
│ │ ├── FluentExtensions.cs
│ │ ├── LayoutTest.cs
│ │ └── SolidBlock.cs
│ ├── TranslateTests.cs
│ └── Usings.cs
├── QuestPDF.ReportSample/
│ ├── DataSource.cs
│ ├── Helpers.cs
│ ├── Layouts/
│ │ ├── DifferentHeadersTemplate.cs
│ │ ├── Helpers.cs
│ │ ├── ImagePlaceholder.cs
│ │ ├── PhotoTemplate.cs
│ │ ├── SectionTemplate.cs
│ │ ├── StandardReport.cs
│ │ └── TableOfContentsTemplate.cs
│ ├── Models.cs
│ ├── QuestPDF.ReportSample.csproj
│ ├── Tests.cs
│ └── Typography.cs
├── QuestPDF.UnitTests/
│ ├── AlignmentTests.cs
│ ├── AspectRatioTests.cs
│ ├── ColumnTests.cs
│ ├── ConstrainedTests.cs
│ ├── DecorationTests.cs
│ ├── DocumentCompressionTests.cs
│ ├── DocumentOperationTests.cs
│ ├── DynamicImageTests.cs
│ ├── EnsureSpaceTests.cs
│ ├── ExtendTests.cs
│ ├── ExternalLinkTests.cs
│ ├── FontManagerTests.cs
│ ├── ImageGenerationTests.cs
│ ├── ImageTests.cs
│ ├── InternalLinkTests.cs
│ ├── InternalLocationTests.cs
│ ├── LayersTests.cs
│ ├── LicenseSetup.cs
│ ├── LineTests.cs
│ ├── PaddingTests.cs
│ ├── PageBreakTests.cs
│ ├── QuestPDF.UnitTests.csproj
│ ├── RotateTests.cs
│ ├── RowTests.cs
│ ├── ScaleTests.cs
│ ├── ShowEntireTests.cs
│ ├── ShowOnceTest.cs
│ ├── SimpleRotateTests.cs
│ ├── StyledBoxTests.cs
│ ├── TestEngine/
│ │ ├── ElementMock.cs
│ │ ├── MockCanvas.cs
│ │ ├── OperationBase.cs
│ │ ├── OperationRecordingCanvas.cs
│ │ ├── Operations/
│ │ │ ├── CanvasDrawImageOperation.cs
│ │ │ ├── CanvasDrawRectangleOperation.cs
│ │ │ ├── CanvasDrawTextOperation.cs
│ │ │ ├── CanvasRotateOperation.cs
│ │ │ ├── CanvasScaleOperation.cs
│ │ │ ├── CanvasTranslateOperation.cs
│ │ │ ├── ChildDrawOperation.cs
│ │ │ ├── ChildMeasureOperation.cs
│ │ │ └── ElementMeasureOperation.cs
│ │ ├── SimpleContainerTests.cs
│ │ └── TestPlan.cs
│ ├── TextSpanTests.cs
│ ├── TextStyleTests.cs
│ ├── TranslateTests.cs
│ ├── UnconstrainedTests.cs
│ └── UnitConversionTests.cs
├── QuestPDF.VisualTests/
│ ├── LineTests.cs
│ ├── QuestPDF.VisualTests.csproj
│ ├── RotateTests.cs
│ ├── SimpleRotateTests.cs
│ ├── StyledBoxTests.cs
│ ├── TestsSetup.cs
│ ├── TextStyleTests.cs
│ └── VisualTestEngine.cs
├── QuestPDF.ZUGFeRD/
│ ├── GenerationTest.cs
│ ├── QuestPDF.ZUGFeRD.csproj
│ ├── resource-factur-x.xml
│ └── resource-zugferd-metadata.xml
├── QuestPDF.slnx
├── global.json
└── nuget.config
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at marcin@ziabek.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: .github/FUNDING.yml
================================================
github: QuestPDF
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Please provide an example, minimalistic code that shows the problem.
Providing working example or example repository greatly helps us investigate the problem and react faster.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment**
What version of the library do you use?
What operating system do you use? (OS type, x64 vs x86 vs arm64)
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/companion_app_feedback.md
================================================
---
name: Companion App Feedback
about: Suggest an idea for this project
title: ''
labels: 'companion-app'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/SECURITY.md
================================================
# Security Policy
## Supported Versions
| Version | Supported | Scope | License |
|-----------|--------------------|----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
| 2024.X.Y | :white_check_mark: | full: feature, quality and bug-fix updates ([branch](https://github.com/QuestPDF/QuestPDF)) | Hybrid: Community MIT, Professional, Enterprise |
| 2023.X.Y | :white_check_mark: | limited until Q4 2024: quality and bug-fix updates ([branch](https://github.com/QuestPDF/QuestPDF)) | Hybrid: Community MIT, Professional, Enterprise |
| 2022.12.X | :x: | supported until Q2 2024, no longer supported ([branch](https://github.com/QuestPDF/QuestPDF/tree/2022.12.X-support)) | MIT |
| Older | :x: | no support | MIT |
## Reporting a Vulnerability
Please report any code vulnerabilities using GitHub Issues. We are trying to respond within 1 day and analyze reported situation as soon as possible.
Please collaborate with us to find best solutation available.
Once the vulnerability is recognized and fixed, the issue is closed with appropraite message.
================================================
FILE: .github/workflows/main.yml
================================================
name: Build, Test And Create Nuget Package
permissions: {}
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
main:
name: ${{ matrix.runtime.name }}
runs-on: ${{ matrix.runtime.runs-on }}
container: ${{ matrix.runtime.container }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
runtime:
- name: win-x64
runs-on: windows-latest-xlarge
# - name: win-x86
# runs-on: windows-latest-xlarge
- name: linux-x64
runs-on: ubuntu-latest-xlarge
container: ubuntu:24.04
- name: linux-arm64
runs-on: ubuntu-latest-xlarge-arm64
container: ubuntu:24.04
- name: linux-musl-x64
runs-on: ubuntu-latest-xlarge
container: alpine:3.20
- name: osx-x64
runs-on: macos-latest-large
- name: osx-arm64
runs-on: macos-latest-xlarge
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Install Build Tools (Linux)
if: matrix.runtime.name == 'linux-x64' || matrix.runtime.name == 'linux-arm64'
shell: sh
run: |
apt update --yes
apt upgrade --yes
# required by actions/setup-dotnet
apt install bash wget --yes
# required by conformance testing tools: veraPDF and mustang
apt install unzip default-jre --yes
java -version
- name: Install Build Tools (Alpine)
if: matrix.runtime.name == 'linux-musl-x64'
shell: sh
run: |
apk update
apk upgrade
# required by actions/setup-dotnet
apk add bash wget
# required by dotnet build command
apk add libstdc++ libgcc
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Install veraPDF - PDF conformance testing tool
if: matrix.runtime.name == 'linux-x64'
run: |
mkdir -p ~/verapdf
cd ~/verapdf
wget -q https://software.verapdf.org/rel/verapdf-installer.zip
unzip -q verapdf-installer.zip
rm verapdf-installer.zip
mv verapdf* verapdf
cd verapdf
printf "1\n\nO\n1\nY\nY\nN\n1\nY\n\n" | ./verapdf-install
alias verapdf='/root/verapdf/verapdf'
verapdf --version
- name: Install mustang - ZUGFeRD conformance testing tool
if: matrix.runtime.name == 'linux-x64'
run: |
mkdir -p /root/mustang
cd /root/mustang
wget https://repo1.maven.org/maven2/org/mustangproject/Mustang-CLI/2.20.0/Mustang-CLI-2.20.0.jar -O mustang-cli.jar
alias mustang='java -jar /root/mustang/mustang-cli.jar'
mustang --help
- name: Build and test solution
shell: bash
working-directory: ./Source
env:
VERAPDF_EXECUTABLE_PATH: '/root/verapdf/verapdf'
MUSTANG_EXECUTABLE_PATH: '/root/mustang/mustang-cli.jar'
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1
run: |
dotnet build --configuration Release --property WarningLevel=0
dotnet test QuestPDF.UnitTests --configuration Release --runtime ${{ matrix.runtime.name }} --framework net10.0
dotnet test QuestPDF.LayoutTests --configuration Release --runtime ${{ matrix.runtime.name }} --framework net10.0
dotnet test QuestPDF.VisualTests --configuration Release --runtime ${{ matrix.runtime.name }} --framework net10.0
dotnet test QuestPDF.DocumentationExamples --configuration Release --runtime ${{ matrix.runtime.name }} --framework net10.0
dotnet test QuestPDF.ReportSample --configuration Release --runtime ${{ matrix.runtime.name }} --framework net10.0
dotnet test QuestPDF.ZUGFeRD --configuration Release --runtime ${{ matrix.runtime.name }} --framework net10.0
if [ "${{ matrix.runtime.name }}" == "linux-x64" ]; then
dotnet test QuestPDF.ConformanceTests --configuration Release --runtime ${{ matrix.runtime.name }} --framework net10.0
fi
dotnet build QuestPDF/QuestPDF.csproj --configuration Release --property WarningLevel=0 --property BUILD_PACKAGE=true
TEST_EXECUTION_PATH='QuestPDF.ReportSample/bin/Release/net10.0/${{ matrix.runtime.name }}'
mkdir -p testOutput/${{ matrix.runtime.name }}
cp -r $TEST_EXECUTION_PATH/report.pdf testOutput/${{ matrix.runtime.name }}
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: questpdf-test-results-${{ matrix.runtime.name }}
path: |
**/*.pdf
- name: Upload nuget artifacts
uses: actions/upload-artifact@v4
if: ${{ matrix.runtime.name == 'win-x64' }}
with:
name: questpdf-nuget-package
path: |
**/*.nupkg
**/*.snupkg
!.nuget
merge:
runs-on: ubuntu-latest
needs: main
steps:
- name: Merge Artifacts
uses: actions/upload-artifact/merge@v4
with:
name: questpdf-test-results
pattern: questpdf-test-results-*
delete-merged: true
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
artifacts/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
*.ncrunchsolution
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Project Rider
*.iml
.idea
# macOS
.DS_Store
**/.DS_Store
================================================
FILE: LICENSE.md
================================================
# QuestPDF License
## License Selection Guide
Welcome to QuestPDF! This guide will help you understand how to select the appropriate license for our library, based on your usage context.
The licensing options for QuestPDF include the MIT license (which is free), and two tiers of paid licenses: the Professional License and the Enterprise License.
### License Equality
We believe in offering the full power of QuestPDF to all our users, regardless of the license they choose. Whether you're operating under our Community MIT, Professional, or Enterprise licenses, you can enjoy the same comprehensive range of features:
- Full access to all QuestPDF features.
- Support for commercial usage.
- Freedom to create and deploy unlimited closed-source projects, applications, and APIs.
- Royalty-free redistribution of the compiled library with your applications.
Transitive Dependency Usage
If you're using QuestPDF as a transitive dependency, you're free to use it under the MIT license without any cost. However, you're welcomed and encouraged to support the project by purchasing a paid license if you find the library valuable.
### Non-profit Usage
If you represent an open-source project, a charitable organization, or are using QuestPDF for evaluation, learning or training purposes, you can also use QuestPDF for free under the MIT license.
### Small Businesses
For companies generating less than $1M USD in annual gross revenue, you can use QuestPDF under the MIT license for free.
### Larger Businesses
Companies with an annual gross revenue exceeding $1M USD are required to purchase a paid license. The type of license you need depends on the number of developers working on projects that use QuestPDF:
Professional License - If there are up to 10 developers in your company who are using QuestPDF, you need to purchase the Professional License.
Enterprise License - If your company has more than 10 developers using QuestPDF, the Enterprise License is the right choice.
### Beyond Compliance
Remember, purchasing a license isn't just about adhering to our guidelines, but also supporting the development of QuestPDF. Your contribution helps us to improve the library and offer top-notch support to all users.
## QuestPDF Community MIT License
### License Permissions
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
### Copyright
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
### Limitation Of Liability
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## QuestPDF Professional and Enterprise Use License
### Do No Harm
By downloading or using the Software, the Licensee agrees not to utilize the software in a manner which is disparaging to QuestPDF, and not to rent, lease or otherwise transfer rights to the Software.
### License Permissions
Grants the use of the Software by a specified number of developers to create and deploy closed-source software for unlimited end user organizations ("The Organization") in multiple locations. This license covers unlimited applications or projects. The Software may be deployed upon any number of machines for the end-use of The Organization. This license also intrinsically covers for development, staging and production servers for each project.
Grants the right to distribute the Software (without royalty) as part of packaged commercial products.
### License Fees
A. If you wish to use the Software in a production environment, the purchase of a license is required. This license is perpetual, granting you continued use of the Software in accordance with the terms and conditions of this Agreement. The cost of the license is as indicated on the pricing page.
B. Upon purchasing a license, you are also enrolled in a yearly, recurring subscription for software updates. This subscription is valid for a period of one year from the date of purchase, and it will automatically renew each year unless cancelled. We recommend maintaining your subscription as long as you are performing active software development to ensure you have access to the latest updates and improvements to the Software.
C. However, it should be noted that the perpetual license allows use of only the latest library revision available at the time of or within the active subscription period, in accordance with the terms and conditions of this Agreement.
D. If you wish to use the Software in a non-production environment, such as for testing and evaluation purposes, you may download and access the source and/or binaries at no charge. This access is subject to all license limitations and restrictions set forth in this Agreement.
### Ownership
QuestPDF shall at all times retain ownership of the QuestPDF Software library and all subsequent copies.
### Copyright
Title, ownership rights, and intellectual property rights in and to the Software shall remain with QuestPDF. The Software is protected by the international copyright laws. Title, ownership rights, and intellectual property rights in and to the content accessed through the Software is the property of the applicable content owner and may be protected by applicable copyright or other law. This License gives you no rights to such content.
### Limitation Of Liability
THIS SOFTWARE IS PROVIDED "AS IS," WITHOUT A WARRANTY OF ANY KIND. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. QUESTPDF AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL QUESTPDF OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF QUESTPDF HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
================================================
FILE: README.md
================================================
## Modern PDF library for C# developers
QuestPDF is a production-ready library that lets you design documents the way you design software: with clean, maintainable, strong-typed C# code.
Stop fighting with HTML-to-PDF conversion. Build pixel-perfect reports, invoices, and exports using the language and tools you already love.
[](https://github.com/QuestPDF/QuestPDF)
[](https://www.nuget.org/packages/QuestPDF/)
[](https://www.questpdf.com/license.html)
[Home Page](https://www.questpdf.com)
•
[Quick Start](https://www.questpdf.com/quick-start.html)
•
[Real-world Invoice Tutorial](https://www.questpdf.com/invoice-tutorial.html)
•
[Features Overview](https://www.questpdf.com/features-overview.html)
•
[License & Pricing](https://www.questpdf.com/license/)
•
[NuGet](https://www.nuget.org/packages/QuestPDF)
## 🚀 Quick start
Learn how easy it is to design, implement and generate PDF documents using QuestPDF.
Effortlessly create documents of all types such as invoices and reports.
```c#
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
// set your license here:
// QuestPDF.Settings.License = LicenseType.Community;
Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hello PDF!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Text(Placeholders.LoremIpsum());
x.Item().Image(Placeholders.Image(200, 100));
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("Page ");
x.CurrentPageNumber();
});
});
})
.GeneratePdf("hello.pdf");
```
The code above produces the following PDF document:
[](https://www.questpdf.com/quick-start.html)
> [!TIP]
> The library is free for individuals, non-profits, all FOSS projects, and organizations under $1M in annual revenue.
> Read more about licensing [here](https://www.questpdf.com/license/)
## Installation
QuestPDF is available as a NuGet package. You can install it through your IDE by searching for phrase `QuestPDF`. If you are not familiar how to do that, please refer to the following guides:
- [Visual Studio](https://learn.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio)
- [Visual Code](https://code.visualstudio.com/docs/csharp/package-management)
- [JetBrains Rider](https://www.jetbrains.com/help/rider/Using_NuGet.html)
Or use the following command in your terminal:
```bash
dotnet add package QuestPDF
```
## Everything you need to generate PDFs
From layout and styling to production features, QuestPDF gives you the flexibility to create documents of any complexity.
### 🎨 Visual Content:
- Page attributes (header, footer, background, watermark, margin),
- Text (font style, paragraph style, page numbers),
- Styled containers (background, border, rounded corners, colors and gradients, shadows),
- Lines (vertical and horizontal, colors and gradients, dash pattern)
- Images (PNG, JPG, WEBP, SVG),
### 🔀 Layout:
- Tables,
- Lists,
- Layers,
- Column / Row,
- Inlined,
### 📐 Positional:
- Alignment,
- Size Controls (width / height),
- Padding,
### 🛠️ Other:
- Page Breaking Control,
- Aspect Ratio,
- Integrations (maps, charts, barcodes, QR codes),
- Hyperlinks,
- Z-index,
[](https://www.questpdf.com/features-overview.html)
## Familiar Programming Patterns
Use your existing programming language and patterns to ship faster with less training.
Loops, conditionals, functions are natively supported. Leverage IntelliSense, inspections, navigation, and safe refactoring.
```csharp
container.Column(column =>
{
column.Item().Text("Order Items").Bold();
if (Model.ShowSummary)
column.Item().Element(ComposeOrderSummary);
foreach (var item in Model.Items)
column.Item().Element(c => ComposeItem(c, item));
});
```
Review document changes like any other code. Get clean diffs, PR approvals, and traceable history.
```diff
void ComposeItem(IContainer container, OrderItem item)
{
container
.Border(1, Colors.Grey.Darken2)
.Background(item.HighlightColor)
- .Padding(12)
+ .Padding(16)
.Row(row =>
{
row.RelativeItem().Text(item.Name);
row.AutoItem().Text($"{item.Price:F2} USD");
});
}
```
## Companion App
Accelerate development with live document preview and hot-reload capability. See your changes instantly without recompiling.
- Explore PDF document hierarchy and navigate its structure
- Quickly magnify and measure content
- Debug runtime exceptions with stack traces and code snippets
- Identify, understand and solve layout errors
[]([https://www.questpdf.com/companion/features.html](https://www.questpdf.com/companion/usage.html))
[](https://www.questpdf.com/companion/features.html)
## Enterprise-grade foundations
- **Predictable Development** — Eliminate CSS debugging, browser quirks, and layout surprises common with HTML-to-PDF tools. What you code is what you get.
- **Source-available** - Entire QuestPDF source code is available for review and customization, ensuring transparency and compliance with your organization's requirements.
- **Complete Data Privacy** - QuestPDF runs entirely within your infrastructure with no external API calls, internet requirement, or background data collection. As a company, we do not access, collect, store, or process your private data.
- **Comprehensive Layout Engine** - A powerful layout engine built specifically for PDF generation. Gain full control over document structure, precise content positioning, and automatic pagination.
- **Advanced Language Support** - Create multilingual documents with full RTL language support, advanced text shaping, and bi-directional layout handling.
- **High Performance** - Generate thousands of pages per second while maintaining minimal CPU and memory usage. Perfect for high-throughput enterprise applications.
- **Optimized File Size** - Drastically reduce file sizes without compromising quality. Benefit from automatic font subsetting, optimal image compression, and efficient file compression.
## Perform common PDF operations
Leverage a powerful C# Fluent API to create, customize, and manage your PDF documents with ease.
- Merge documents
- Attach files
- Extract pages
- Encrypt / decrypt
- Extend metadata
– Limit access
- Optimize for Web
- Overlay / underlay
```c#
DocumentOperation
.LoadFile("input.pdf")
.TakePages("1-10")
.MergeFile("appendix.pdf", "1-z") // all pages
.AddAttachment(new DocumentAttachment
{
FilePath = "metadata.xml"
})
.Encrypt(new Encryption256Bit
{
OwnerPassword = "mypassword",
AllowPrinting = true,
AllowContentExtraction = false
})
.Save("final-document.pdf");
```
[](https://www.questpdf.com/concepts/document-operations.html)
## Works everywhere you do
Deploy on any major operating system and integrate seamlessly with your favorite IDEs, cloud platforms, and development tools.
| Platform | Support |
|----------|---------|
| **Operating Systems** | Windows, Linux, macOS |
| **Frameworks** | .NET 6+ and .NET Framework 4.6.2+ |
| **Cloud** | Azure, AWS, Google Cloud, Others |
| **Containers** | Docker, Kubernetes |
| **IDEs** | Visual Studio, VS Code, JetBrains Rider, Others |
## Industry-standard PDF compliance
Generate PDF documents that meet the strictest archival and accessibility requirements. Every build is automatically validated using the open-source veraPDF and Mustang tools.
- PDF/A (Archival):
- Purpose: ISO 19005 standard for long-term preservation. Ensures PDFs remain readable and visually identical for decades without external dependencies.
- Supported Standards: `PDF/A-2b`, `PDF/A-2u`, `PDF/A-2a`, `PDF/A-3b`, `PDF/A-3u`, `PDF/A-3a`
- PDF/UA (Accessibility):
- Purpose: ISO 14289 standard for universal accessibility. Includes full support for screen readers and assistive technologies for people with disabilities.
- Supported Standards: `PDF/UA-1`
- EN 16931 (E-Invoicing):
- Purpose: European standard for electronic invoicing. Embeds structured invoice data (XML) within PDF documents for automated processing.
- Supported Standards: `ZUGFeRD`, `Factur-X`
## Fair and Sustainable License
A model that benefits everyone. Commercial licensing provides businesses with legal safety and long-term stability, while funding a feature-complete, unrestricted library for the open-source community.
- Actively maintained with regular feature, quality, and security updates
- Full source code available on GitHub
- All features included in every tier without restrictions
- Predictable pricing: no per-seat, per-server, or usage fees
> [!TIP]
> Free for individuals, non-profits, all FOSS projects, and organizations under $1M in annual revenue.
[](https://www.questpdf.com/license.html)
[](https://www.questpdf.com/license/guide.html)
## See a real-world example
Follow our detailed tutorial and see how easy it is to generate a fully functional invoice with fewer than 250 lines of C# code.
- Step-by-step guidance
- Production-ready code
- Best practices included
[](https://www.questpdf.com/invoice-tutorial.html)
## Community QuestPDF
We are incredibly grateful to our .NET Community for their positive reviews and recommendations of the QuestPDF library.
Your support and feedback are invaluable and motivate us to keep improving and expanding this project.
Thank you for helping us grow and reach more developers!
### Nick Chapsas: The Easiest Way to Create PDFs in .NET
[](https://www.youtube.com/watch?v=_M0IgtGWnvE)
### JetBrains: OSS Power-Ups: QuestPDF
[](https://www.youtube.com/watch?v=-iYvZvpLX0g)
## Please help by giving a star ⭐
GitHub stars guide developers toward great tools. If you find this project valuable, please give it a star – it helps the community and takes just a second!
================================================
FILE: Source/.config/dotnet-tools.json
================================================
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-stryker": {
"version": "3.11.1",
"commands": [
"dotnet-stryker"
]
}
}
}
================================================
FILE: Source/.config/stryker-config.json
================================================
{
"stryker-config":
{
"solution": "../QuestPDF.sln",
"project": "QuestPDF/QuestPDF.csproj",
"test-projects": ["../QuestPDF.LayoutTests/QuestPDF.LayoutTests.csproj"],
"mutate": ["**/*Column.cs"],
"reporters": [
"progress",
"html"
],
"disable-mix-mutants": true,
"disable-bail": true
}
}
================================================
FILE: Source/.editorconfig
================================================
# Rules in this file were initially inferred by Visual Studio IntelliCode from the D:\GithubRepos\QuestPDF codebase based on best match to current usage at 16.03.2022
# You can modify the rules from these initially generated values to suit your own policies
# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
[*.cs]
#Core editorconfig formatting - indentation
#use soft tabs (spaces) for indentation
indent_style = space
#Formatting - new line options
#place catch statements on a new line
csharp_new_line_before_catch = true
#place else statements on a new line
csharp_new_line_before_else = true
#require members of anonymous types to be on separate lines
csharp_new_line_before_members_in_anonymous_types = true
#require members of object intializers to be on separate lines
csharp_new_line_before_members_in_object_initializers = true
#require braces to be on a new line for object_collection_array_initializers, methods, anonymous_types, control_blocks, types, and lambdas (also known as "Allman" style)
csharp_new_line_before_open_brace =all
#Formatting - organize using options
#sort System.* using directives alphabetically, and place them before other usings
dotnet_sort_system_directives_first = true
#Formatting - spacing options
#require NO space between a cast and the value
csharp_space_after_cast = false
#require a space before the colon for bases or interfaces in a type declaration
csharp_space_after_colon_in_inheritance_clause = true
#require a space after a keyword in a control flow statement such as a for loop
csharp_space_after_keywords_in_control_flow_statements = true
#require a space before the colon for bases or interfaces in a type declaration
csharp_space_before_colon_in_inheritance_clause = true
#remove space within empty argument list parentheses
csharp_space_between_method_call_empty_parameter_list_parentheses = false
#remove space between method call name and opening parenthesis
csharp_space_between_method_call_name_and_opening_parenthesis = false
#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call
csharp_space_between_method_call_parameter_list_parentheses = false
#remove space within empty parameter list parentheses for a method declaration
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list.
csharp_space_between_method_declaration_parameter_list_parentheses = false
#Formatting - wrapping options
#leave code block on single line
csharp_preserve_single_line_blocks = true
#Style - Code block preferences
#prefer no curly braces if allowed
csharp_prefer_braces = false:suggestion
#Style - expression bodied member options
#prefer block bodies for constructors
csharp_style_expression_bodied_constructors = false:suggestion
#prefer block bodies for methods
csharp_style_expression_bodied_methods = false:suggestion
#prefer expression-bodied members for properties
csharp_style_expression_bodied_properties = true:suggestion
#Style - expression level options
#prefer out variables to be declared inline in the argument list of a method call when possible
csharp_style_inlined_variable_declaration = true:suggestion
#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them
dotnet_style_predefined_type_for_member_access = true:suggestion
#Style - Expression-level preferences
#prefer objects to be initialized using object initializers when possible
dotnet_style_object_initializer = true:suggestion
#prefer inferred anonymous type member names
dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion
#prefer inferred tuple element names
dotnet_style_prefer_inferred_tuple_names = true:suggestion
#Style - implicit and explicit types
#prefer var over explicit type in all cases, unless overridden by another code style rule
csharp_style_var_elsewhere = true:suggestion
#prefer var is used to declare variables with built-in system types such as int
csharp_style_var_for_built_in_types = true:suggestion
#prefer var when the type is already mentioned on the right-hand side of a declaration expression
csharp_style_var_when_type_is_apparent = true:suggestion
#Style - language keyword and framework type options
#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
#Style - Miscellaneous preferences
#prefer local functions over anonymous functions
csharp_style_pattern_local_over_anonymous_function = true:suggestion
#Style - modifier options
#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods.
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
#Style - Modifier preferences
#when this rule is set to a list of modifiers, prefer the specified ordering.
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
#Style - Pattern matching
#prefer pattern matching instead of is expression with type casts
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
#Style - qualification options
#prefer fields not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_field = false:suggestion
#prefer methods not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_method = false:suggestion
#prefer properties not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_property = false:suggestion
[*.{cs,vb}]
tab_width=4
indent_size=4
================================================
FILE: Source/QuestPDF/Build/QuestPDF.targets
================================================
$(TargetFramework.StartsWith('net4'))
LatoFont\%(RecursiveDir)%(Filename)%(Extension)
PreserveNewest
================================================
FILE: Source/QuestPDF/Build/net4/QuestPDF.targets
================================================
runtimes\%(RecursiveDir)%(Filename)%(Extension)
PreserveNewest
================================================
FILE: Source/QuestPDF/Companion/CompanionExtensions.cs
================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Companion
{
public static class CompanionExtensions
{
static CompanionExtensions()
{
LicenseChecker.ValidateLicense();
}
#if NET6_0_OR_GREATER
///
public static void ShowInCompanion(this IDocument document, int port = 12500)
{
document.ShowInCompanionAsync(port).ConfigureAwait(true).GetAwaiter().GetResult();
}
///
public static async Task ShowInCompanionAsync(this IDocument document, int port = 12500, CancellationToken cancellationToken = default)
{
Settings.EnableCaching = false;
Settings.EnableDebugging = true;
if (document is MergedDocument)
throw new NotSupportedException("The QuestPDF Companion App does not currently support merged documents. Please use the tool with a single document at a time.");
var companionService = new CompanionService(port);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
companionService.OnCompanionStopped += () => cancellationTokenSource.Cancel();
await companionService.Connect();
companionService.StartRenderRequestedPageSnapshotsTask(cancellationToken);
await RefreshPreview();
HotReloadManager.UpdateApplicationRequested += (_, _) =>
{
CompanionService.IsDocumentHotReloaded = true;
RefreshPreview();
};
await KeepApplicationAlive(cancellationTokenSource.Token);
Task RefreshPreview()
{
try
{
var pictures = DocumentGenerator.GenerateCompanionContent(document);
return companionService.RefreshPreview(pictures);
}
catch (Exception exception)
{
return companionService.InformAboutGenericException(exception);
}
}
async Task KeepApplicationAlive(CancellationToken cancellationToken)
{
while (true)
{
if (cancellationToken.IsCancellationRequested)
return;
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
}
#else
///
public static void ShowInCompanion(this IDocument document, int port = 12500)
{
throw new Exception("The hot-reload feature requires .NET 6 or later.");
}
///
public static async Task ShowInCompanionAsync(this IDocument document, int port = 12500, CancellationToken cancellationToken = default)
{
throw new Exception("The hot-reload feature requires .NET 6 or later.");
}
#endif
}
}
================================================
FILE: Source/QuestPDF/Companion/CompanionModels.cs
================================================
using System;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Companion
{
sealed class PageSnapshotIndex
{
public int PageIndex { get; set; }
public int ZoomLevel { get; set; }
public override string ToString() => $"{ZoomLevel}/{PageIndex}";
}
static internal class CompanionCommands
{
internal sealed class Notify
{
private static readonly string CurrentClientId = Guid.NewGuid().ToString();
public string ClientId => CurrentClientId;
public LicenseType License => Settings.License ?? LicenseType.Community;
}
internal sealed class UpdateDocumentStructure
{
public bool IsDocumentHotReloaded { get; set; }
public ICollection Pages { get; set; }
public DocumentHierarchyElement Hierarchy { get; set; }
public sealed class PageSize
{
public float Width { get; set; }
public float Height { get; set; }
}
internal sealed record DocumentHierarchyElement
{
internal Element Element { get; set; }
public string ElementType { get; set; }
public string? Hint { get; set; }
public string? SearchableContent { get; set; }
public bool IsSingleChildContainer { get; set; }
public ICollection PageLocations { get; set; }
public ICollection LayoutErrorMeasurements { get; set; }
public ICollection Properties { get; set; }
public SourceCodePath? SourceCodeDeclarationPath { get; set; }
public ICollection Children { get; set; }
}
internal sealed class PageLocation
{
public int PageNumber { get; init; }
public float Left { get; init; }
public float Top { get; init; }
public float Right { get; init; }
public float Bottom { get; init; }
}
internal sealed class LayoutErrorMeasurement
{
public int PageNumber { get; set; }
public ElementSize? AvailableSpace { get; set; }
public ElementSize? MeasurementSize { get; set; }
public SpacePlanType? SpacePlanType { get; set; }
public string? WrapReason { get; set; }
public bool IsLayoutErrorRootCause { get; set; }
}
internal sealed class SourceCodePath
{
public string FilePath { get; set; }
public int LineNumber { get; set; }
}
}
internal sealed class ProvideRenderedDocumentPage
{
public ICollection Pages { get; set; }
internal sealed class RenderedPage
{
public int PageIndex { get; set; }
public int ZoomLevel { get; set; }
public string ImageData { get; set; } // base64
}
}
internal sealed class ShowGenericException
{
public GenericExceptionDetails Exception { get; set; }
internal sealed class GenericExceptionDetails
{
public string Type { get; set; }
public string Message { get; set; }
public ICollection StackTrace { get; set; }
public GenericExceptionDetails? InnerException { get; set; }
}
internal sealed class StackFrame
{
public string CodeLocation { get; set; }
public string? FileName { get; set; }
public int? LineNumber { get; set; }
}
}
internal sealed class ElementSize
{
public float Width { get; set; }
public float Height { get; set; }
}
internal sealed class ElementProperty
{
public string Label { get; set; }
public string Value { get; set; }
}
internal sealed class GetVersionCommandResponse
{
public ICollection SupportedVersions { get; set; }
}
}
}
================================================
FILE: Source/QuestPDF/Companion/CompanionService.cs
================================================
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
using QuestPDF.Drawing.DocumentCanvases;
namespace QuestPDF.Companion
{
internal sealed class CompanionService
{
private int Port { get; }
private HttpClient HttpClient { get; }
public event Action? OnCompanionStopped;
private const int RequiredCompanionApiVersion = 3;
private static CompanionDocumentSnapshot? CurrentDocumentSnapshot { get; set; }
public static bool IsCompanionAttached { get; private set; }
public static bool IsDocumentHotReloaded { get; set; } = false;
JsonSerializerOptions JsonSerializerOptions = new()
{
MaxDepth = 256,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
public CompanionService(int port)
{
IsCompanionAttached = true;
Port = port;
HttpClient = new()
{
BaseAddress = new Uri($"http://localhost:{port}/"),
Timeout = TimeSpan.FromSeconds(5)
};
}
public async Task Connect()
{
await CheckIfCompanionIsRunning();
await CheckCompanionVersionCompatibility();
StartNotifyPresenceTask();
}
private async Task CheckIfCompanionIsRunning()
{
try
{
using var result = await HttpClient.GetAsync("/ping");
result.EnsureSuccessStatusCode();
}
catch
{
throw new Exception("Cannot connect to the QuestPDF Companion tool. Please ensure that the tool is running and the port is correct. Learn more: https://www.questpdf.com/companion/usage.html");
}
}
internal async Task StartNotifyPresenceTask()
{
while (true)
{
try
{
using var result = await HttpClient.PostAsJsonAsync($"/v{RequiredCompanionApiVersion}/notify", new CompanionCommands.Notify(), JsonSerializerOptions);
}
catch
{
}
await Task.Delay(TimeSpan.FromMilliseconds(250));
}
}
private async Task CheckCompanionVersionCompatibility()
{
using var result = await HttpClient.GetAsync("/version");
var response = await result.Content.ReadFromJsonAsync();
if (response.SupportedVersions.Contains(RequiredCompanionApiVersion))
return;
throw new Exception($"The QuestPDF Companion application is not compatible. Please install the QuestPDF Companion tool in a proper version.");
}
public async Task RefreshPreview(CompanionDocumentSnapshot companionDocumentSnapshot)
{
// clean old state
if (CurrentDocumentSnapshot != null)
{
foreach (var companionPageSnapshot in CurrentDocumentSnapshot.Pictures)
companionPageSnapshot.Picture.Dispose();
}
// set new state
CurrentDocumentSnapshot = companionDocumentSnapshot;
var documentStructure = new CompanionCommands.UpdateDocumentStructure
{
Hierarchy = companionDocumentSnapshot.Hierarchy.ImproveHierarchyStructure(),
IsDocumentHotReloaded = IsDocumentHotReloaded,
Pages = companionDocumentSnapshot
.Pictures
.Select(x => new CompanionCommands.UpdateDocumentStructure.PageSize
{
Width = x.Size.Width,
Height = x.Size.Height
})
.ToArray()
};
await HttpClient.PostAsJsonAsync($"/v{RequiredCompanionApiVersion}/documentPreview/update", documentStructure, JsonSerializerOptions);
}
public void StartRenderRequestedPageSnapshotsTask(CancellationToken cancellationToken)
{
Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await RenderRequestedPageSnapshots();
}
catch
{
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken);
}
}
});
}
private async Task RenderRequestedPageSnapshots()
{
// get requests
var getRequestedSnapshots = await HttpClient.GetAsync($"/v{RequiredCompanionApiVersion}/documentPreview/getRenderingRequests");
getRequestedSnapshots.EnsureSuccessStatusCode();
var requestedSnapshots = await getRequestedSnapshots.Content.ReadFromJsonAsync>();
if (!requestedSnapshots.Any())
return;
if (CurrentDocumentSnapshot == null)
return;
// render snapshots
if (!requestedSnapshots.Any())
return;
var renderingTasks = requestedSnapshots
.Select(index => Task.Run(() =>
{
var image = CurrentDocumentSnapshot
.Pictures
.ElementAt(index.PageIndex)
.RenderImage(index.ZoomLevel);
return new CompanionCommands.ProvideRenderedDocumentPage.RenderedPage
{
PageIndex = index.PageIndex,
ZoomLevel = index.ZoomLevel,
ImageData = Convert.ToBase64String(image)
};
}))
.ToList();
var renderedPages = await Task.WhenAll(renderingTasks);
var command = new CompanionCommands.ProvideRenderedDocumentPage { Pages = renderedPages };
await HttpClient.PostAsJsonAsync($"/v{RequiredCompanionApiVersion}/documentPreview/provideRenderedImages", command);
}
internal async Task InformAboutGenericException(Exception exception)
{
var command = new CompanionCommands.ShowGenericException
{
Exception = Map(exception)
};
await HttpClient.PostAsJsonAsync($"/v{RequiredCompanionApiVersion}/genericException/show", command, JsonSerializerOptions);
return;
static CompanionCommands.ShowGenericException.GenericExceptionDetails Map(Exception exception)
{
return new CompanionCommands.ShowGenericException.GenericExceptionDetails
{
Type = exception.GetType().FullName ?? "Unknown",
Message = exception.Message,
StackTrace = exception.StackTrace.ParseStackTrace(),
InnerException = exception.InnerException == null ? null : Map(exception.InnerException)
};
}
}
}
}
#endif
================================================
FILE: Source/QuestPDF/Companion/Helpers.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Companion;
internal static class CompanionModelHelpers
{
internal static CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement ExtractHierarchy(this Element container)
{
var layoutTree = container.ExtractElementsOfType().Single();
return Traverse(layoutTree);
CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement Traverse(TreeNode node)
{
var child = node.Value.Child;
while (child is ElementProxy elementProxy)
child = elementProxy.Child;
if (child is Container)
return Traverse(node.Children.Single());
var hierarchyElement = MapToHierarchyElement(node);
// special case to optimize the hierarchy structure in the Companion App
if (child is StyledBox styledBox)
{
var customContentChildren = styledBox
.GetCompanionCustomContent()
.Select(x => hierarchyElement with { ElementType = x.Type, Hint = x.Hint })
.ToArray();
if (customContentChildren.Length == 0)
return hierarchyElement;
for (var i = 0; i <= customContentChildren.Length - 2; i++)
customContentChildren[i].Children = [customContentChildren[i + 1]];
return customContentChildren.First();
}
return hierarchyElement;
}
CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement MapToHierarchyElement(TreeNode node)
{
var layout = node.Value;
var child = layout.Child;
while (child is ElementProxy proxy)
child = proxy.Child;
return new CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement
{
Element = child,
ElementType = child.GetType().Name,
Hint = child.GetCompanionHint(),
SearchableContent = child.GetCompanionSearchableContent(),
PageLocations = layout.Snapshots,
SourceCodeDeclarationPath = GetSourceCodePath(child.CodeLocation),
LayoutErrorMeasurements = layout.LayoutErrorMeasurements,
IsSingleChildContainer = child is ContainerElement,
Properties = child.GetCompanionProperties()?.Select(x => new CompanionCommands.ElementProperty { Label = x.Key, Value = x.Value }).ToList() ?? [],
Children = node.Children.Select(Traverse).ToList()
};
}
}
private static CompanionCommands.UpdateDocumentStructure.SourceCodePath? GetSourceCodePath(SourceCodePath? path)
{
if (path == null)
return null;
return new CompanionCommands.UpdateDocumentStructure.SourceCodePath
{
FilePath = path.Value.FilePath,
LineNumber = path.Value.LineNumber
};
}
#if NET6_0_OR_GREATER
internal static CompanionCommands.ShowGenericException.StackFrame[] ParseStackTrace(this string stackTrace)
{
var lines = stackTrace.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var frames = new List();
foreach (string line in lines)
{
var fullMatch = Regex.Match(line, @"at\s+(?.+)\s+in\s(?.+)\s*:line\s(?\d+)");
var codeOnlyMatch = Regex.Match(line, @"at\s+(?.+)");
if (fullMatch.Success)
{
frames.Add(new CompanionCommands.ShowGenericException.StackFrame
{
CodeLocation = fullMatch.Groups["codeLocation"].Value,
FileName = fullMatch.Groups["fileName"].Value,
LineNumber = int.Parse(fullMatch.Groups["lineNumber"].Value)
});
}
else if (codeOnlyMatch.Success)
{
frames.Add(new CompanionCommands.ShowGenericException.StackFrame
{
CodeLocation = codeOnlyMatch.Groups["codeLocation"].Value
});
}
}
return frames.ToArray();
}
#endif
internal static CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement ImproveHierarchyStructure(this CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement root)
{
var document = FindDocumentStructurePointersThat(root, x => x == DocumentStructureTypes.Document).Single();
document.IsSingleChildContainer = false;
var pages = FindDocumentStructurePointersThat(document, x => x == DocumentStructureTypes.Page).ToList();
foreach (var page in pages)
{
page.IsSingleChildContainer = false;
page.Children = FindDocumentStructurePointersThat(page, x => x is not (DocumentStructureTypes.Document or DocumentStructureTypes.Page)).ToList();
}
document.Children = pages;
if (pages.Count == 1)
document.Children = pages.Single().Children;
return document;
ICollection FindDocumentStructurePointersThat(CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement root, Predicate predicate)
{
var result = new List();
Traverse(root);
return result;
void Traverse(CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement element)
{
if (element.Element is DebugPointer { Type: DebugPointerType.DocumentStructure } debugPointer && Enum.TryParse(debugPointer.Label, out var structureType) && predicate(structureType))
{
result.Add(element);
return;
}
foreach (var child in element.Children)
Traverse(child);
}
}
}
}
================================================
FILE: Source/QuestPDF/Companion/HotReloadManager.cs
================================================
#if NET6_0_OR_GREATER
using System;
using QuestPDF.Companion;
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(HotReloadManager))]
namespace QuestPDF.Companion
{
///
/// Helper to subscribe to hot reload notifications.
///
internal static class HotReloadManager
{
public static event EventHandler? UpdateApplicationRequested;
public static void UpdateApplication(Type[]? _)
{
UpdateApplicationRequested?.Invoke(null, EventArgs.Empty);
}
}
}
#endif
================================================
FILE: Source/QuestPDF/Companion/Previewer.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Companion;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer;
public static class PreviewerExtensions
{
private const string ObsoleteMessage = "The Previewer application is no longer supprted. Please use a new QuestPDF Companion application by calling ShowInCompanion() or ShowInCompanionAsync() methods.";
#if NET6_0_OR_GREATER
[Obsolete(ObsoleteMessage)]
[ExcludeFromCodeCoverage]
public static void ShowInPreviewer(this IDocument document, int port = 12500)
{
throw new NotImplementedException(ObsoleteMessage);
}
[Obsolete(ObsoleteMessage)]
[ExcludeFromCodeCoverage]
public static Task ShowInPreviewerAsync(this IDocument document, int port = 12500, CancellationToken cancellationToken = default)
{
throw new NotImplementedException(ObsoleteMessage);
}
#else
[Obsolete(ObsoleteMessage)]
[ExcludeFromCodeCoverage]
public static void ShowInPreviewer(this IDocument document, int port = 12500)
{
throw new NotSupportedException(ObsoleteMessage);
}
[Obsolete(ObsoleteMessage)]
[ExcludeFromCodeCoverage]
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500, CancellationToken cancellationToken = default)
{
throw new NotSupportedException(ObsoleteMessage);
}
#endif
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentCanvases/CompanionDocumentCanvas.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using QuestPDF.Companion;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Drawing.DocumentCanvases
{
internal sealed class CompanionPageSnapshot
{
public SkPicture Picture { get; set; }
public Size Size { get; set; }
public CompanionPageSnapshot(SkPicture picture, Size size)
{
Picture = picture;
Size = size;
}
public byte[] RenderImage(int zoomLevel)
{
// prepare canvas
var scale = (float)Math.Pow(2, zoomLevel);
using var bitmap = new SkBitmap((int)(Size.Width * scale), (int)(Size.Height * scale));
using var canvas = SkCanvas.CreateFromBitmap(bitmap);
canvas.Scale(scale, scale);
// draw white background
using var backgroundPaint = new SkPaint();
backgroundPaint.SetSolidColor(Colors.White);
var backgroundRect = new SkRect(0, 0, Size.Width, Size.Height);
canvas.DrawRectangle(backgroundRect, backgroundPaint);
// draw content
canvas.DrawPicture(Picture);
// export as image
using var encodedBitmapData = bitmap.EncodeAsJpeg(90);
return encodedBitmapData.ToBytes();
}
}
internal sealed class CompanionDocumentSnapshot
{
public ICollection Pictures { get; set; }
public CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement Hierarchy { get; set; }
}
internal sealed class CompanionDocumentCanvas : IDocumentCanvas, IDisposable
{
private ProxyDrawingCanvas DrawingCanvas { get; } = new();
private Size CurrentPageSize { get; set; } = Size.Zero;
private ICollection PageSnapshots { get; } = new List();
internal CompanionCommands.UpdateDocumentStructure.DocumentHierarchyElement Hierarchy { get; set; }
public CompanionDocumentSnapshot GetContent()
{
return new CompanionDocumentSnapshot
{
Pictures = PageSnapshots,
Hierarchy = Hierarchy
};
}
#region IDisposable
~CompanionDocumentCanvas()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
DrawingCanvas.Dispose();
GC.SuppressFinalize(this);
}
#endregion
#region IDocumentCanvas
public void SetSemanticTree(SemanticTreeNode? semanticTree)
{
}
public void BeginDocument()
{
PageSnapshots.Clear();
}
public void EndDocument()
{
}
public void BeginPage(Size size)
{
CurrentPageSize = size;
DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
DrawingCanvas.SetZIndex(0);
}
public void EndPage()
{
Debug.Assert(!CurrentPageSize.IsCloseToZero());
using var pictureRecorder = new SkPictureRecorder();
using var canvas = pictureRecorder.BeginRecording(CurrentPageSize.Width, CurrentPageSize.Height);
using var snapshot = DrawingCanvas.GetSnapshot();
snapshot.DrawOnSkCanvas(canvas);
canvas.Save();
var picture = pictureRecorder.EndRecording();
PageSnapshots.Add(new CompanionPageSnapshot(picture, CurrentPageSize));
}
public IDrawingCanvas GetDrawingCanvas()
{
return DrawingCanvas;
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentCanvases/DiscardDocumentCanvas.cs
================================================
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.DocumentCanvases;
internal sealed class DiscardDocumentCanvas : IDocumentCanvas
{
private DiscardDrawingCanvas DrawingCanvas { get; } = new();
public void SetSemanticTree(SemanticTreeNode? semanticTree)
{
}
public void BeginDocument()
{
}
public void EndDocument()
{
}
public void BeginPage(Size size)
{
}
public void EndPage()
{
}
public IDrawingCanvas GetDrawingCanvas()
{
return DrawingCanvas;
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentCanvases/ImageDocumentCanvas.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Drawing.DocumentCanvases
{
internal sealed class ImageDocumentCanvas : IDocumentCanvas, IDisposable
{
private ImageGenerationSettings Settings { get; }
private SkBitmap Bitmap { get; set; }
private SkCanvas? CurrentPageCanvas { get; set; }
private ProxyDrawingCanvas DrawingCanvas { get; } = new();
internal ICollection Images { get; } = new List();
public ImageDocumentCanvas(ImageGenerationSettings settings)
{
Settings = settings;
}
#region IDisposable
~ImageDocumentCanvas()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
CurrentPageCanvas?.Dispose();
Bitmap?.Dispose();
DrawingCanvas?.Dispose();
GC.SuppressFinalize(this);
}
#endregion
#region IDocumentCanvas
public void SetSemanticTree(SemanticTreeNode? semanticTree)
{
}
public void BeginDocument()
{
}
public void EndDocument()
{
CurrentPageCanvas?.Dispose();
Bitmap?.Dispose();
}
public void BeginPage(Size size)
{
var scalingFactor = Settings.RasterDpi / (float) PageSizes.PointsPerInch;
Bitmap = new SkBitmap((int) (size.Width * scalingFactor), (int) (size.Height * scalingFactor));
CurrentPageCanvas = SkCanvas.CreateFromBitmap(Bitmap);
CurrentPageCanvas.Scale(scalingFactor, scalingFactor);
if (Settings.ImageFormat == ImageFormat.Jpeg)
{
using var whitePaint = new SkPaint();
whitePaint.SetSolidColor(Colors.White);
CurrentPageCanvas.DrawRectangle(new SkRect(0, 0, size.Width, size.Height), whitePaint);
}
DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
DrawingCanvas.SetZIndex(0);
}
public void EndPage()
{
Debug.Assert(CurrentPageCanvas != null);
using var documentPageSnapshot = DrawingCanvas.GetSnapshot();
documentPageSnapshot.DrawOnSkCanvas(CurrentPageCanvas);
CurrentPageCanvas.Save();
CurrentPageCanvas.Dispose();
CurrentPageCanvas = null;
using var imageData = EncodeBitmap();
var imageBytes = imageData.ToBytes();
Images.Add(imageBytes);
Bitmap.Dispose();
SkData EncodeBitmap()
{
return Settings.ImageFormat switch
{
ImageFormat.Jpeg => Bitmap.EncodeAsJpeg(Settings.ImageCompressionQuality.ToQualityValue()),
ImageFormat.Png => Bitmap.EncodeAsPng(),
ImageFormat.Webp => Bitmap.EncodeAsWebp(Settings.ImageCompressionQuality.ToQualityValue()),
_ => throw new ArgumentOutOfRangeException()
};
}
}
public IDrawingCanvas GetDrawingCanvas()
{
return DrawingCanvas;
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentCanvases/PdfDocumentCanvas.cs
================================================
using System;
using System.Diagnostics;
using System.Linq;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Drawing.DocumentCanvases
{
internal sealed class PdfDocumentCanvas : IDocumentCanvas, IDisposable
{
private SkWriteStream WriteStream { get; }
private DocumentMetadata DocumentMetadata { get; }
private DocumentSettings DocumentSettings { get; }
private SkPdfTag? SemanticTag { get; set; }
private SkDocument? Document { get; set; }
private SkCanvas? CurrentPageCanvas { get; set; }
private ProxyDrawingCanvas DrawingCanvas { get; } = new();
public PdfDocumentCanvas(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
{
WriteStream = stream;
DocumentMetadata = documentMetadata;
DocumentSettings = documentSettings;
}
private SkDocument CreatePdf()
{
// do not extract to another method, as it will cause the SkText objects
// to be disposed before the SkPdfDocument is created
using var title = new SkText(DocumentMetadata.Title);
using var author = new SkText(DocumentMetadata.Author);
using var subject = new SkText(DocumentMetadata.Subject);
using var keywords = new SkText(DocumentMetadata.Keywords);
using var creator = new SkText(DocumentMetadata.Creator);
using var producer = new SkText(DocumentMetadata.Producer);
using var language = new SkText(DocumentMetadata.Language);
var internalMetadata = new SkPdfDocumentMetadata
{
Title = title,
Author = author,
Subject = subject,
Keywords = keywords,
Creator = creator,
Producer = producer,
Language = language,
CreationDate = new SkDateTime(DocumentMetadata.CreationDate),
ModificationDate = new SkDateTime(DocumentMetadata.ModifiedDate),
PDFA_Conformance = GetPDFAConformanceLevel(DocumentSettings.PDFA_Conformance),
PDFUA_Conformance = GetPDFUAConformanceLevel(DocumentSettings.PDFUA_Conformance),
RasterDPI = DocumentSettings.ImageRasterDpi,
CompressDocument = DocumentSettings.CompressDocument,
SemanticNodeRoot = SemanticTag?.Instance ?? IntPtr.Zero
};
try
{
return SkPdfDocument.Create(WriteStream, internalMetadata);
}
catch (TypeInitializationException exception)
{
throw new InitializationException("PDF", exception);
}
}
static Skia.PDFA_Conformance GetPDFAConformanceLevel(Infrastructure.PDFA_Conformance conformanceLevel)
{
return conformanceLevel switch
{
Infrastructure.PDFA_Conformance.None => Skia.PDFA_Conformance.None,
// Infrastructure.PDFA_Conformance.PDFA_1A => Skia.PDFA_Conformance.PDFA_1A,
// Infrastructure.PDFA_Conformance.PDFA_1B => Skia.PDFA_Conformance.PDFA_1B,
Infrastructure.PDFA_Conformance.PDFA_2A => Skia.PDFA_Conformance.PDFA_2A,
Infrastructure.PDFA_Conformance.PDFA_2B => Skia.PDFA_Conformance.PDFA_2B,
Infrastructure.PDFA_Conformance.PDFA_2U => Skia.PDFA_Conformance.PDFA_2U,
Infrastructure.PDFA_Conformance.PDFA_3A => Skia.PDFA_Conformance.PDFA_3A,
Infrastructure.PDFA_Conformance.PDFA_3B => Skia.PDFA_Conformance.PDFA_3B,
Infrastructure.PDFA_Conformance.PDFA_3U => Skia.PDFA_Conformance.PDFA_3U,
_ => throw new ArgumentOutOfRangeException(nameof(conformanceLevel), conformanceLevel, "Unsupported PDF/A conformance level")
};
}
static Skia.PDFUA_Conformance GetPDFUAConformanceLevel(Infrastructure.PDFUA_Conformance conformanceLevel)
{
return conformanceLevel switch
{
Infrastructure.PDFUA_Conformance.None => Skia.PDFUA_Conformance.None,
Infrastructure.PDFUA_Conformance.PDFUA_1 => Skia.PDFUA_Conformance.PDFUA_1,
_ => throw new ArgumentOutOfRangeException(nameof(conformanceLevel), conformanceLevel, "Unsupported PDF/UA conformance level")
};
}
#region IDisposable
~PdfDocumentCanvas()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
Document?.Dispose();
CurrentPageCanvas?.Dispose();
DrawingCanvas?.Dispose();
SemanticTag?.Dispose();
// don't dispose WriteStream - its lifetime is managed externally
GC.SuppressFinalize(this);
}
#endregion
#region IDocumentCanvas
public void SetSemanticTree(SemanticTreeNode? semanticTree)
{
if (semanticTree == null)
{
SemanticTag?.Dispose();
SemanticTag = null;
return;
}
SemanticTag = Convert(semanticTree);
static SkPdfTag Convert(SemanticTreeNode node)
{
var result = SkPdfTag.Create(node.NodeId, node.Type, node.Alt, node.Lang);
var children = node.Children.Select(Convert).ToArray();
result.SetChildren(children);
foreach (var nodeAttribute in node.Attributes)
result.AddAttribute(nodeAttribute.Owner, nodeAttribute.Name, nodeAttribute.Value);
return result;
}
}
public void BeginDocument()
{
Document ??= CreatePdf();
}
public void EndDocument()
{
Document?.Close();
Document?.Dispose();
}
public void BeginPage(Size size)
{
CurrentPageCanvas = Document?.BeginPage(size.Width, size.Height);
DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
DrawingCanvas.SetZIndex(0);
}
public void EndPage()
{
Debug.Assert(CurrentPageCanvas != null);
using var documentPageSnapshot = DrawingCanvas.GetSnapshot();
documentPageSnapshot.DrawOnSkCanvas(CurrentPageCanvas);
CurrentPageCanvas.Save();
CurrentPageCanvas.Dispose();
CurrentPageCanvas = null;
Document.EndPage();
}
public IDrawingCanvas GetDrawingCanvas()
{
return DrawingCanvas;
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentCanvases/SemanticDocumentCanvas.cs
================================================
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.DocumentCanvases;
internal sealed class SemanticDocumentCanvas : IDocumentCanvas
{
private SemanticDrawingCanvas DrawingCanvas { get; } = new();
public void SetSemanticTree(SemanticTreeNode? semanticTree)
{
}
public void BeginDocument()
{
}
public void EndDocument()
{
}
public void BeginPage(Size size)
{
}
public void EndPage()
{
}
public IDrawingCanvas GetDrawingCanvas()
{
return DrawingCanvas;
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentCanvases/SvgDocumentCanvas.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Drawing.DocumentCanvases
{
internal sealed class SvgDocumentCanvas : IDocumentCanvas, IDisposable
{
private SkCanvas? CurrentPageCanvas { get; set; }
private ProxyDrawingCanvas DrawingCanvas { get; } = new();
private MemoryStream WriteStream { get; set; }
private SkWriteStream SkiaStream { get; set; }
internal ICollection Images { get; } = new List();
#region IDisposable
~SvgDocumentCanvas()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
CurrentPageCanvas?.Dispose();
WriteStream?.Dispose();
SkiaStream?.Dispose();
DrawingCanvas?.Dispose();
GC.SuppressFinalize(this);
}
#endregion
#region IDocumentCanvas
public void SetSemanticTree(SemanticTreeNode? semanticTree)
{
}
public void BeginDocument()
{
}
public void EndDocument()
{
CurrentPageCanvas?.Dispose();
WriteStream?.Dispose();
SkiaStream?.Dispose();
}
public void BeginPage(Size size)
{
WriteStream?.Dispose();
SkiaStream?.Dispose();
WriteStream = new MemoryStream();
SkiaStream = new SkWriteStream(WriteStream);
CurrentPageCanvas = SkSvgCanvas.CreateSvg(size.Width, size.Height, SkiaStream);
DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
DrawingCanvas.SetZIndex(0);
}
public void EndPage()
{
Debug.Assert(CurrentPageCanvas != null);
using var documentPageSnapshot = DrawingCanvas.GetSnapshot();
documentPageSnapshot.DrawOnSkCanvas(CurrentPageCanvas);
CurrentPageCanvas.Save();
CurrentPageCanvas.Dispose();
CurrentPageCanvas = null;
SkiaStream.Flush();
var data = WriteStream.ToArray();
var svgImage = Encoding.UTF8.GetString(data);
Images.Add(svgImage);
SkiaStream.Dispose();
WriteStream.Dispose();
}
public IDrawingCanvas GetDrawingCanvas()
{
return DrawingCanvas;
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentCanvases/XpsDocumentCanvas.cs
================================================
using System;
using System.Diagnostics;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Drawing.DocumentCanvases
{
internal sealed class XpsDocumentCanvas : IDocumentCanvas, IDisposable
{
private SkDocument Document { get; }
private SkCanvas? CurrentPageCanvas { get; set; }
private ProxyDrawingCanvas DrawingCanvas { get; } = new();
public XpsDocumentCanvas(SkWriteStream stream, DocumentSettings documentSettings)
{
Document = CreateXps(stream, documentSettings);
}
private static SkDocument CreateXps(SkWriteStream stream, DocumentSettings documentSettings)
{
try
{
return SkXpsDocument.Create(stream, documentSettings.ImageRasterDpi);
}
catch (TypeInitializationException exception)
{
throw new InitializationException("XPS", exception);
}
}
#region IDisposable
~XpsDocumentCanvas()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
Document?.Dispose();
CurrentPageCanvas?.Dispose();
DrawingCanvas?.Dispose();
GC.SuppressFinalize(this);
}
#endregion
#region IDocumentCanvas
public void SetSemanticTree(SemanticTreeNode? semanticTree)
{
}
public void BeginDocument()
{
}
public void EndDocument()
{
Document?.Close();
Document?.Dispose();
}
public void BeginPage(Size size)
{
CurrentPageCanvas = Document?.BeginPage(size.Width, size.Height);
DrawingCanvas.Target = new SkiaDrawingCanvas(size.Width, size.Height);
DrawingCanvas.SetZIndex(0);
}
public void EndPage()
{
Debug.Assert(CurrentPageCanvas != null);
using var documentPageSnapshot = DrawingCanvas.GetSnapshot();
documentPageSnapshot.DrawOnSkCanvas(CurrentPageCanvas);
CurrentPageCanvas.Save();
CurrentPageCanvas.Dispose();
CurrentPageCanvas = null;
Document.EndPage();
}
public IDrawingCanvas GetDrawingCanvas()
{
return DrawingCanvas;
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentContainer.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing
{
internal sealed class DocumentContainer : IDocumentContainer
{
internal List Pages { get; set; } = [];
internal Container Compose()
{
var container = new Container();
ComposeContainer(container);
return container;
void ComposeContainer(IContainer container)
{
if (Pages.Count == 0)
Pages.Add(new Page());
container = container.DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Document.ToString());
if (Pages.Count == 1)
{
container.Component(Pages.First());
return;
}
container
.Column(column =>
{
Pages
.SelectMany(x => new List()
{
() => column.Item().PageBreak(),
() => column.Item().SemanticTag("Part").Component(x)
})
.Skip(1)
.ToList()
.ForEach(x => x());
});
}
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentGenerator.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Companion;
using QuestPDF.Drawing.DocumentCanvases;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
using QuestPDF.Elements.Text;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
using PDFA_Conformance = QuestPDF.Infrastructure.PDFA_Conformance;
using PDFUA_Conformance = QuestPDF.Infrastructure.PDFUA_Conformance;
namespace QuestPDF.Drawing
{
static class DocumentGenerator
{
static DocumentGenerator()
{
SkNativeDependencyCompatibilityChecker.Test();
}
internal static void GeneratePdf(SkWriteStream stream, IDocument document)
{
LicenseChecker.ValidateLicense();
var metadata = document.GetMetadata();
var settings = document.GetSettings();
if (Settings.License == LicenseType.Evaluation)
metadata.Producer = "QuestPDF (Evaluation Mode)";
using var canvas = new PdfDocumentCanvas(stream, metadata, settings);
RenderDocument(canvas, document, settings);
}
internal static void GenerateXps(SkWriteStream stream, IDocument document)
{
LicenseChecker.ValidateLicense();
var settings = document.GetSettings();
using var canvas = new XpsDocumentCanvas(stream, settings);
RenderDocument(canvas, document, settings);
}
internal static ICollection GenerateImages(IDocument document, ImageGenerationSettings imageGenerationSettings)
{
LicenseChecker.ValidateLicense();
var documentSettings = document.GetSettings();
documentSettings.ImageRasterDpi = imageGenerationSettings.RasterDpi;
using var canvas = new ImageDocumentCanvas(imageGenerationSettings);
RenderDocument(canvas, document, documentSettings);
return canvas.Images;
}
internal static ICollection GenerateSvg(IDocument document)
{
LicenseChecker.ValidateLicense();
using var canvas = new SvgDocumentCanvas();
RenderDocument(canvas, document, document.GetSettings());
return canvas.Images;
}
internal static CompanionDocumentSnapshot GenerateCompanionContent(IDocument document)
{
using var canvas = new CompanionDocumentCanvas();
RenderDocument(canvas, document, DocumentSettings.Default);
return canvas.GetContent();
}
internal static void RenderDocument(IDocumentCanvas canvas, IDocument document, DocumentSettings settings)
{
if (document is MergedDocument mergedDocument)
{
RenderMergedDocument(canvas, mergedDocument, settings);
return;
}
var semanticTreeManager = CreateSemanticTreeManager(settings);
var useOriginalImages = canvas is ImageDocumentCanvas;
var content = ConfigureContent(document, settings, semanticTreeManager, useOriginalImages);
if (canvas is CompanionDocumentCanvas)
content.VisitChildren(x => x.CreateProxy(y => new LayoutProxy(y)));
try
{
var pageContext = new PageContext();
RenderPass(pageContext, new SemanticDocumentCanvas(), content);
pageContext.ProceedToNextRenderingPhase();
canvas.ConfigureWithSemanticTree(semanticTreeManager);
canvas.BeginDocument();
RenderPass(pageContext, canvas, content);
canvas.EndDocument();
if (canvas is CompanionDocumentCanvas companionCanvas)
companionCanvas.Hierarchy = content.ExtractHierarchy();
}
finally
{
content.ReleaseDisposableChildren();
}
}
private static void RenderMergedDocument(IDocumentCanvas canvas, MergedDocument document, DocumentSettings settings)
{
var useOriginalImages = canvas is ImageDocumentCanvas;
var sharedPageContent = new PageContext();
var useSharedPageContext = document.PageNumberStrategy == MergedDocumentPageNumberStrategy.Continuous;
var semanticTreeManager = CreateSemanticTreeManager(settings);
var semanticDocumentCanvas = new SemanticDocumentCanvas();
var documentParts = Enumerable
.Range(0, document.Documents.Count)
.Select(index => new
{
DocumentId = index,
Content = ConfigureContent(document.Documents[index], settings, semanticTreeManager, useOriginalImages),
PageContext = useSharedPageContext ? sharedPageContent : new PageContext()
})
.ToList();
try
{
foreach (var documentPart in documentParts)
{
documentPart.PageContext.SetDocumentId(documentPart.DocumentId);
RenderPass(documentPart.PageContext, semanticDocumentCanvas, documentPart.Content);
}
foreach (var documentPart in documentParts)
documentPart.PageContext.ProceedToNextRenderingPhase();
canvas.ConfigureWithSemanticTree(semanticTreeManager);
canvas.BeginDocument();
foreach (var documentPart in documentParts)
{
documentPart.PageContext.SetDocumentId(documentPart.DocumentId);
RenderPass(documentPart.PageContext, canvas, documentPart.Content);
documentPart.Content.ReleaseDisposableChildren();
}
canvas.EndDocument();
}
finally
{
documentParts.ForEach(x => x.Content.ReleaseDisposableChildren());
}
}
private static SemanticTreeManager? CreateSemanticTreeManager(DocumentSettings settings)
{
return IsDocumentSemanticAware() ? new SemanticTreeManager() : null;
bool IsDocumentSemanticAware()
{
if (settings.PDFUA_Conformance is not PDFUA_Conformance.None)
return true;
//if (settings.PDFA_Conformance is PDFA_Conformance.PDFA_1A or PDFA_Conformance.PDFA_2A or PDFA_Conformance.PDFA_3A)
if (settings.PDFA_Conformance is PDFA_Conformance.PDFA_2A or PDFA_Conformance.PDFA_3A)
return true;
return false;
}
}
private static void ConfigureWithSemanticTree(this IDocumentCanvas canvas, SemanticTreeManager? semanticTreeManager)
{
if (semanticTreeManager == null)
return;
var semanticTree = semanticTreeManager.GetSemanticTree();
semanticTreeManager.Reset();
canvas.SetSemanticTree(semanticTree);
}
private static Container ConfigureContent(IDocument document, DocumentSettings settings, SemanticTreeManager? semanticTreeManager, bool useOriginalImages)
{
var container = new DocumentContainer();
document.Compose(container);
var content = container.Compose();
content.ApplyInheritedAndGlobalTexStyle(TextStyle.Default);
content.ApplyContentDirection(settings.ContentDirection);
content.ApplyDefaultImageConfiguration(settings.ImageRasterDpi, settings.ImageCompressionQuality, useOriginalImages);
if (Settings.EnableCaching)
content.ApplyCaching();
if (semanticTreeManager != null)
{
content.ApplySemanticParagraphs();
content.InjectSemanticTreeManager(semanticTreeManager);
}
return content;
}
private static void RenderPass(PageContext pageContext, IDocumentCanvas canvas, ContainerElement content)
{
content.InjectDependencies(pageContext, canvas.GetDrawingCanvas());
content.VisitChildren(x => (x as IStateful)?.ResetState(hardReset: true));
while(true)
{
pageContext.IncrementPageNumber();
var spacePlan = content.Measure(Size.Max);
if (spacePlan.Type == SpacePlanType.Wrap)
{
pageContext.DecrementPageNumber();
canvas.EndDocument();
#if NET6_0_OR_GREATER
if (!CompanionService.IsCompanionAttached)
ThrowLayoutException();
#else
ThrowLayoutException();
#endif
ApplyLayoutDebugging();
}
try
{
canvas.BeginPage(spacePlan);
content.Draw(spacePlan);
}
catch (Exception exception)
{
canvas.EndDocument();
throw new DocumentDrawingException("An exception occured during document drawing.", exception);
}
canvas.EndPage();
if (spacePlan.Type == SpacePlanType.FullRender)
break;
}
void ApplyLayoutDebugging()
{
content.VisitChildren(x => (x as SnapshotCacheRecorderProxy)?.Dispose());
content.RemoveExistingProxiesOfType();
content.ApplyLayoutOverflowDetection();
content.Measure(Size.Max);
var overflowState = content.ExtractElementsOfType().Single();
overflowState.StopMeasuring();
overflowState.TryToFixTheLayoutOverflowIssue();
content.ApplyContentDirection();
content.InjectDependencies(pageContext, canvas.GetDrawingCanvas());
content.VisitChildren(x => (x as LayoutProxy)?.CaptureLayoutErrorMeasurement());
content.RemoveExistingProxiesOfType();
}
void ThrowLayoutException()
{
var newLine = "\n";
var newParagraph = newLine + newLine;
const string debuggingSettingsName = $"{nameof(QuestPDF)}.{nameof(Settings)}.{nameof(Settings.EnableDebugging)}";
var message =
$"The provided document content contains conflicting size constraints. " +
$"For example, some elements may require more space than is available. {newParagraph}";
if (Settings.EnableDebugging)
{
var (ancestors, layout) = GenerateLayoutExceptionDebuggingInfo();
var ancestorsText = ancestors.FormatAncestors();
var layoutText = layout.FormatLayoutSubtree();
message +=
$"The layout issue is likely present in the following part of the document: {newParagraph}{ancestorsText}{newParagraph}" +
$"To learn more, please analyse the document measurement of the problematic location: {newParagraph}{layoutText}" +
$"{LayoutDebugging.LayoutVisualizationLegend}{newParagraph}" +
$"This detailed information is generated because you run the application with a debugger attached or with the {debuggingSettingsName} flag set to true. ";
}
else
{
message +=
$"To further investigate the location of the root cause, please run the application with a debugger attached or set the {debuggingSettingsName} flag to true. " +
$"The library will generate additional debugging information such as probable code problem location and detailed layout measurement overview.";
}
throw new DocumentLayoutException(message);
}
(ICollection ancestors, TreeNode layout) GenerateLayoutExceptionDebuggingInfo()
{
content.RemoveExistingProxies();
content.ApplyLayoutOverflowDetection();
content.Measure(Size.Max);
var overflowState = content.ExtractElementsOfType().Single();
overflowState.StopMeasuring();
overflowState.TryToFixTheLayoutOverflowIssue();
var rootCause = overflowState.FindLayoutOverflowVisualizationNodes().First();
var ancestors = rootCause
.ExtractAncestors()
.Select(x => x.Value.Child)
.Where(x => x is DebugPointer or SourceCodePointer)
.Reverse()
.ToArray();
var layout = rootCause
.ExtractAncestors()
.First(x => x.Value.Child is SourceCodePointer or DebugPointer)
.Children
.First();
return (ancestors, layout);
}
}
internal static void InjectSemanticTreeManager(this Element content, SemanticTreeManager semanticTreeManager)
{
content.VisitChildren(x =>
{
if (x is ISemanticAware semanticAware)
{
semanticAware.SemanticTreeManager = semanticTreeManager;
}
else if (x is TextBlock textBlock)
{
foreach (var textBlockElement in textBlock.Items.OfType())
{
textBlockElement.Element.InjectSemanticTreeManager(semanticTreeManager);
}
}
});
}
internal static void InjectDependencies(this Element content, IPageContext pageContext, IDrawingCanvas canvas)
{
content.VisitChildren(x =>
{
if (x == null)
return;
x.PageContext = pageContext;
x.Canvas = canvas;
});
}
internal static void ApplyCaching(this Element? content)
{
var canApplyCaching = Traverse(content);
if (canApplyCaching)
content?.CreateProxy(x => new SnapshotCacheRecorderProxy(x));
// returns true if can apply caching
bool Traverse(Element? content)
{
if (content is TextBlock textBlock)
{
foreach (var textBlockItem in textBlock.Items)
{
if (textBlockItem is TextBlockPageNumber)
return false;
if (textBlockItem is TextBlockElement textBlockElement && !Traverse(textBlockElement.Element))
return false;
}
return true;
}
if (content is Lazy lazy)
return lazy.IsCacheable;
if (content is DynamicHost)
return false;
if (content is ContainerElement containerElement)
return Traverse(containerElement.Child);
if (content is MultiColumn multiColumn)
{
var multiColumnSupportsCaching = Traverse(multiColumn.Content) && Traverse(multiColumn.Spacer);
multiColumn.Content.RemoveExistingProxies();
multiColumn.Spacer.RemoveExistingProxies();
return multiColumnSupportsCaching;
}
var canApplyCachingPerChild = content.GetChildren().Select(Traverse).ToArray();
if (canApplyCachingPerChild.All(x => x))
return true;
if (content is Row row && row.Items.Any(x => x.Type == RowItemType.Auto))
return false;
var childIndex = 0;
content.CreateProxy(x =>
{
var canApplyCaching = canApplyCachingPerChild[childIndex];
childIndex++;
return canApplyCaching ? new SnapshotCacheRecorderProxy(x) : x;
});
return false;
}
}
internal static void ApplyContentDirection(this Element? content, ContentDirection? direction = null)
{
if (content == null)
return;
if (content is ContentDirectionSetter contentDirectionSetter)
{
ApplyContentDirection(contentDirectionSetter.Child, contentDirectionSetter.ContentDirection);
return;
}
if (content is IContentDirectionAware contentDirectionAware)
contentDirectionAware.ContentDirection = direction ?? contentDirectionAware.ContentDirection;
foreach (var child in content.GetChildren())
ApplyContentDirection(child, direction);
}
internal static void ApplyDefaultImageConfiguration(this Element? content, int imageRasterDpi, ImageCompressionQuality imageCompressionQuality, bool useOriginalImages)
{
content.VisitChildren(x =>
{
if (x is QuestPDF.Elements.Image image)
{
image.TargetDpi ??= imageRasterDpi;
image.CompressionQuality ??= imageCompressionQuality;
image.UseOriginalImage |= useOriginalImages;
}
if (x is QuestPDF.Elements.DynamicImage dynamicImage)
{
dynamicImage.TargetDpi ??= imageRasterDpi;
dynamicImage.CompressionQuality ??= imageCompressionQuality;
dynamicImage.UseOriginalImage |= useOriginalImages;
}
if (x is DynamicHost dynamicHost)
{
dynamicHost.ImageTargetDpi ??= imageRasterDpi;
dynamicHost.ImageCompressionQuality ??= imageCompressionQuality;
dynamicHost.UseOriginalImage |= useOriginalImages;
}
if (x is Lazy lazy)
{
lazy.ImageTargetDpi ??= imageRasterDpi;
lazy.ImageCompressionQuality ??= imageCompressionQuality;
lazy.UseOriginalImage |= useOriginalImages;
}
if (x is TextBlock textBlock)
{
foreach (var textBlockElement in textBlock.Items.OfType())
{
textBlockElement.Element.ApplyDefaultImageConfiguration(imageRasterDpi, imageCompressionQuality, useOriginalImages);
}
}
});
}
internal static void ApplyInheritedAndGlobalTexStyle(this Element? content, TextStyle documentDefaultTextStyle)
{
if (content == null)
return;
if (content is TextBlock textBlock)
{
textBlock.DefaultTextStyle = textBlock.DefaultTextStyle.ApplyInheritedStyle(documentDefaultTextStyle).ApplyGlobalStyle();
foreach (var textBlockItem in textBlock.Items)
{
if (textBlockItem is TextBlockSpan textSpan)
textSpan.Style = textSpan.Style.ApplyInheritedStyle(documentDefaultTextStyle).ApplyGlobalStyle();
if (textBlockItem is TextBlockElement textElement)
ApplyInheritedAndGlobalTexStyle(textElement.Element, documentDefaultTextStyle);
}
return;
}
if (content is DynamicHost dynamicHost)
dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyInheritedStyle(documentDefaultTextStyle);
if (content is Lazy lazy)
lazy.TextStyle = lazy.TextStyle.ApplyInheritedStyle(documentDefaultTextStyle);
if (content is DefaultTextStyle defaultTextStyleElement)
documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyInheritedStyle(documentDefaultTextStyle);
if (content is ContainerElement containerElement)
{
ApplyInheritedAndGlobalTexStyle(containerElement.Child, documentDefaultTextStyle);
}
else
{
foreach (var child in content.GetChildren())
ApplyInheritedAndGlobalTexStyle(child, documentDefaultTextStyle);
}
}
internal static void ApplySemanticParagraphs(this Element root)
{
var isFooterContext = false;
Traverse(root);
void Traverse(Element element)
{
if (element is SemanticTag { TagType: "H" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6" or "P" or "Lbl" })
{
return;
}
else if (element is ArtifactTag)
{
// ignore all Text elements that are marked as artifacts
}
else if (element is DebugPointer { Type: DebugPointerType.DocumentStructure, Label: nameof(DocumentStructureTypes.Footer) } debugPointer)
{
isFooterContext = true;
Traverse(debugPointer.Child);
isFooterContext = false;
}
else if (element is ContainerElement container)
{
if (container.Child is TextBlock textBlock)
{
var textBlockContainsPageNumber = textBlock.Items.Any(x => x is TextBlockPageNumber);
if (isFooterContext && textBlockContainsPageNumber)
{
container.CreateProxy(x => new ArtifactTag
{
Child = x,
Id = SkSemanticNodeSpecialId.PaginationArtifact
});
}
else
{
container.CreateProxy(x => new SemanticTag
{
Child = x,
TagType = "P"
});
}
}
else
{
Traverse(container.Child);
}
}
else
{
foreach (var child in element.GetChildren())
Traverse(child);
}
}
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/DocumentPageSnapshot.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Skia;
namespace QuestPDF.Drawing;
internal class DocumentPageSnapshot : IDisposable
{
public List Layers { get; init; }
~DocumentPageSnapshot()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
foreach (var layer in Layers)
layer.Picture.Dispose();
Layers.Clear();
GC.SuppressFinalize(this);
}
public class LayerSnapshot
{
public int ZIndex { get; init; }
public SkPicture Picture { get; init; }
}
public void DrawOnSkCanvas(SkCanvas canvas)
{
foreach (var layerSnapshot in Layers.OrderBy(x => x.ZIndex))
canvas.DrawPicture(layerSnapshot.Picture);
}
}
================================================
FILE: Source/QuestPDF/Drawing/DrawingCanvases/DiscardDrawingCanvas.cs
================================================
using System;
using System.Collections.Generic;
using System.Numerics;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
using QuestPDF.Skia.Text;
namespace QuestPDF.Drawing.DrawingCanvases
{
internal sealed class DiscardDrawingCanvas : IDrawingCanvas
{
private Stack MatrixStack { get; } = new();
private Matrix4x4 CurrentMatrix { get; set; } = Matrix4x4.Identity;
private int CurrentZIndex { get; set; } = 0;
public DocumentPageSnapshot GetSnapshot()
{
return new DocumentPageSnapshot();
}
public void DrawSnapshot(DocumentPageSnapshot snapshot)
{
}
public void Save()
{
MatrixStack.Push(CurrentMatrix);
}
public void Restore()
{
CurrentMatrix = MatrixStack.Pop();
}
public void SetZIndex(int index)
{
CurrentZIndex = index;
}
public int GetZIndex()
{
return CurrentZIndex;
}
public SkCanvasMatrix GetCurrentMatrix()
{
return SkCanvasMatrix.FromMatrix4x4(CurrentMatrix);
}
public void SetMatrix(SkCanvasMatrix matrix)
{
CurrentMatrix = matrix.ToMatrix4x4();
}
public void Translate(Position vector)
{
CurrentMatrix = Matrix4x4.CreateTranslation(vector.X, vector.Y, 0) * CurrentMatrix;
}
public void Scale(float scaleX, float scaleY)
{
CurrentMatrix = Matrix4x4.CreateScale(scaleX, scaleY, 1) * CurrentMatrix;
}
public void Rotate(float angle)
{
CurrentMatrix = Matrix4x4.CreateRotationZ((float)Math.PI * angle / 180f) * CurrentMatrix;
}
public void DrawLine(Position start, Position end, SkPaint paint)
{
}
public void DrawRectangle(Position vector, Size size, SkPaint paint)
{
}
public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint)
{
}
public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow)
{
}
public void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo)
{
}
public void DrawImage(SkImage image, Size size)
{
}
public void DrawPicture(SkPicture picture)
{
}
public void DrawSvgPath(string path, Color color)
{
}
public void DrawSvg(SkSvgImage svgImage, Size size)
{
}
public void DrawOverflowArea(SkRect area)
{
}
public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace)
{
}
public void ClipRectangle(SkRect clipArea)
{
}
public void ClipRoundedRectangle(SkRoundedRect clipArea)
{
}
public void DrawHyperlink(Size size, string url, string? description)
{
}
public void DrawSectionLink(Size size, string sectionName, string? description)
{
}
public void DrawSection(string sectionName)
{
}
public int GetSemanticNodeId()
{
return 0;
}
public void SetSemanticNodeId(int nodeId)
{
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/DrawingCanvases/ProxyDrawingCanvas.cs
================================================
using System;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
using QuestPDF.Skia.Text;
namespace QuestPDF.Drawing.DrawingCanvases;
internal sealed class ProxyDrawingCanvas : IDrawingCanvas, IDisposable
{
public IDrawingCanvas Target { get; set; }
#region IDisposable
~ProxyDrawingCanvas()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
(Target as IDisposable)?.Dispose();
GC.SuppressFinalize(this);
}
#endregion
#region IDrawingCanvas
public DocumentPageSnapshot GetSnapshot()
{
return Target.GetSnapshot();
}
public void DrawSnapshot(DocumentPageSnapshot snapshot)
{
Target.DrawSnapshot(snapshot);
}
public void Save()
{
Target.Save();
}
public void Restore()
{
Target.Restore();
}
public void SetZIndex(int index)
{
Target.SetZIndex(index);
}
public int GetZIndex()
{
return Target.GetZIndex();
}
public SkCanvasMatrix GetCurrentMatrix()
{
return Target.GetCurrentMatrix();
}
public void SetMatrix(SkCanvasMatrix matrix)
{
Target.SetMatrix(matrix);
}
public void Translate(Position vector)
{
Target.Translate(vector);
}
public void Scale(float scaleX, float scaleY)
{
Target.Scale(scaleX, scaleY);
}
public void Rotate(float angle)
{
Target.Rotate(angle);
}
public void DrawLine(Position start, Position end, SkPaint paint)
{
Target.DrawLine(start, end, paint);
}
public void DrawRectangle(Position vector, Size size, SkPaint paint)
{
Target.DrawRectangle(vector, size, paint);
}
public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint)
{
Target.DrawComplexBorder(innerRect, outerRect, paint);
}
public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow)
{
Target.DrawShadow(shadowRect, shadow);
}
public void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo)
{
Target.DrawParagraph(paragraph, lineFrom, lineTo);
}
public void DrawImage(SkImage image, Size size)
{
Target.DrawImage(image, size);
}
public void DrawPicture(SkPicture picture)
{
Target.DrawPicture(picture);
}
public void DrawSvgPath(string path, Color color)
{
Target.DrawSvgPath(path, color);
}
public void DrawSvg(SkSvgImage svgImage, Size size)
{
Target.DrawSvg(svgImage, size);
}
public void DrawOverflowArea(SkRect area)
{
Target.DrawOverflowArea(area);
}
public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace)
{
Target.ClipOverflowArea(availableSpace, requiredSpace);
}
public void ClipRectangle(SkRect clipArea)
{
Target.ClipRectangle(clipArea);
}
public void ClipRoundedRectangle(SkRoundedRect clipArea)
{
Target.ClipRoundedRectangle(clipArea);
}
public void DrawHyperlink(Size size, string url, string? description)
{
Target.DrawHyperlink(size, url, description);
}
public void DrawSectionLink(Size size, string sectionName, string? description)
{
Target.DrawSectionLink(size, sectionName, description);
}
public void DrawSection(string sectionName)
{
Target.DrawSection(sectionName);
}
public int GetSemanticNodeId()
{
return Target.GetSemanticNodeId();
}
public void SetSemanticNodeId(int nodeId)
{
Target.SetSemanticNodeId(nodeId);
}
#endregion
}
================================================
FILE: Source/QuestPDF/Drawing/DrawingCanvases/SemanticDrawingCanvas.cs
================================================
using System;
using System.Collections.Generic;
using System.Numerics;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
using QuestPDF.Skia.Text;
namespace QuestPDF.Drawing.DrawingCanvases
{
internal sealed class SemanticDrawingCanvas : IDrawingCanvas
{
private Stack MatrixStack { get; } = new();
private Matrix4x4 CurrentMatrix { get; set; } = Matrix4x4.Identity;
private int CurrentZIndex { get; set; } = 0;
public DocumentPageSnapshot GetSnapshot()
{
return new DocumentPageSnapshot();
}
public void DrawSnapshot(DocumentPageSnapshot snapshot)
{
}
public void Save()
{
MatrixStack.Push(CurrentMatrix);
}
public void Restore()
{
CurrentMatrix = MatrixStack.Pop();
}
public void SetZIndex(int index)
{
CurrentZIndex = index;
}
public int GetZIndex()
{
return CurrentZIndex;
}
public SkCanvasMatrix GetCurrentMatrix()
{
return SkCanvasMatrix.FromMatrix4x4(CurrentMatrix);
}
public void SetMatrix(SkCanvasMatrix matrix)
{
CurrentMatrix = matrix.ToMatrix4x4();
}
public void Translate(Position vector)
{
CurrentMatrix = Matrix4x4.CreateTranslation(vector.X, vector.Y, 0) * CurrentMatrix;
}
public void Scale(float scaleX, float scaleY)
{
CurrentMatrix = Matrix4x4.CreateScale(scaleX, scaleY, 1) * CurrentMatrix;
}
public void Rotate(float angle)
{
CurrentMatrix = Matrix4x4.CreateRotationZ((float)Math.PI * angle / 180f) * CurrentMatrix;
}
public void DrawLine(Position start, Position end, SkPaint paint)
{
}
public void DrawRectangle(Position vector, Size size, SkPaint paint)
{
}
public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint)
{
}
public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow)
{
}
public void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo)
{
}
public void DrawImage(SkImage image, Size size)
{
}
public void DrawPicture(SkPicture picture)
{
}
public void DrawSvgPath(string path, Color color)
{
}
public void DrawSvg(SkSvgImage svgImage, Size size)
{
}
public void DrawOverflowArea(SkRect area)
{
}
public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace)
{
}
public void ClipRectangle(SkRect clipArea)
{
}
public void ClipRoundedRectangle(SkRoundedRect clipArea)
{
}
public void DrawHyperlink(Size size, string url, string? description)
{
}
public void DrawSectionLink(Size size, string sectionName, string? description)
{
}
public void DrawSection(string sectionName)
{
}
public int GetSemanticNodeId()
{
return 0;
}
public void SetSemanticNodeId(int nodeId)
{
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/DrawingCanvases/SkiaDrawingCanvas.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
using QuestPDF.Skia.Text;
namespace QuestPDF.Drawing.DrawingCanvases
{
internal sealed class SkiaDrawingCanvas : IDrawingCanvas, IDisposable
{
public float Width { get; }
public float Height { get; }
public SkiaDrawingCanvas(float width, float height)
{
Width = width;
Height = height;
}
~SkiaDrawingCanvas()
{
Dispose();
}
public void Dispose()
{
CurrentCanvas?.Dispose();
CurrentCanvas = null;
foreach (var layer in ZIndexCanvases.Values)
{
layer.Canvas.Dispose();
layer.PictureRecorder.Dispose();
}
ZIndexCanvases.Clear();
GC.SuppressFinalize(this);
}
#region ZIndex
private SkCanvas CurrentCanvas { get; set; }
private int CurrentZIndex { get; set; } = 0;
private IDictionary ZIndexCanvases { get; } = new Dictionary();
private SkCanvas GetCanvasForZIndex(int zIndex)
{
if (ZIndexCanvases.TryGetValue(zIndex, out var value))
return value.Canvas;
var pictureRecorder = new SkPictureRecorder();
var canvas = pictureRecorder.BeginRecording(Width, Height);
ZIndexCanvases.Add(zIndex, (pictureRecorder, canvas));
return canvas;
}
#endregion
#region ICanvas
public DocumentPageSnapshot GetSnapshot()
{
return new DocumentPageSnapshot
{
Layers = ZIndexCanvases
.Select(zindex =>
{
using var pictureRecorder = zindex.Value.PictureRecorder;
var picture = pictureRecorder.EndRecording();
zindex.Value.Canvas.Dispose();
return new DocumentPageSnapshot.LayerSnapshot
{
ZIndex = zindex.Key,
Picture = picture
};
})
.ToList()
};
}
public void DrawSnapshot(DocumentPageSnapshot snapshot)
{
foreach (var snapshotLayer in snapshot.Layers.OrderBy(x => x.ZIndex))
{
var canvas = GetCanvasForZIndex(snapshotLayer.ZIndex);
canvas.Save();
canvas.SetCurrentMatrix(SkCanvasMatrix.Identity);
canvas.DrawPicture(snapshotLayer.Picture);
canvas.Restore();
}
}
public void Save()
{
CurrentCanvas.Save();
}
public void Restore()
{
CurrentCanvas.Restore();
}
public void SetZIndex(int index)
{
var currentMatrix = CurrentCanvas?.GetCurrentMatrix() ?? SkCanvasMatrix.Identity;
CurrentZIndex = index;
CurrentCanvas = GetCanvasForZIndex(CurrentZIndex);
CurrentCanvas.SetCurrentMatrix(currentMatrix);
}
public int GetZIndex()
{
return CurrentZIndex;
}
public SkCanvasMatrix GetCurrentMatrix()
{
return CurrentCanvas.GetCurrentMatrix();
}
public void SetMatrix(SkCanvasMatrix matrix)
{
CurrentCanvas.SetCurrentMatrix(matrix);
}
public void Translate(Position vector)
{
CurrentCanvas.Translate(vector.X, vector.Y);
}
public void Scale(float scaleX, float scaleY)
{
CurrentCanvas.Scale(scaleX, scaleY);
}
public void Rotate(float angle)
{
CurrentCanvas.Rotate(angle);
}
public void DrawLine(Position start, Position end, SkPaint paint)
{
var startPoint = new SkPoint(start.X, start.Y);
var endPoint = new SkPoint(end.X, end.Y);
CurrentCanvas.DrawLine(startPoint, endPoint, paint);
}
public void DrawRectangle(Position vector, Size size, SkPaint paint)
{
var position = new SkRect(vector.X, vector.Y, vector.X + size.Width, vector.Y + size.Height);
CurrentCanvas.DrawRectangle(position, paint);
}
public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint)
{
CurrentCanvas.DrawComplexBorder(innerRect, outerRect, paint);
}
public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow)
{
CurrentCanvas.DrawShadow(shadowRect, shadow);
}
public void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo)
{
CurrentCanvas.DrawParagraph(paragraph, lineFrom, lineTo);
}
public void DrawImage(SkImage image, Size size)
{
CurrentCanvas.DrawImage(image, size.Width, size.Height);
}
public void DrawPicture(SkPicture picture)
{
CurrentCanvas.DrawPicture(picture);
}
public void DrawSvgPath(string path, Color color)
{
CurrentCanvas.DrawSvgPath(path, color);
}
public void DrawSvg(SkSvgImage svgImage, Size size)
{
CurrentCanvas.DrawSvg(svgImage, size.Width, size.Height);
}
public void DrawOverflowArea(SkRect area)
{
CurrentCanvas.DrawOverflowArea(area);
}
public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace)
{
CurrentCanvas.ClipOverflowArea(availableSpace, requiredSpace);
}
public void ClipRectangle(SkRect clipArea)
{
CurrentCanvas.ClipRectangle(clipArea);
}
public void ClipRoundedRectangle(SkRoundedRect clipArea)
{
CurrentCanvas.ClipRoundedRectangle(clipArea);
}
public void DrawHyperlink(Size size, string url, string? description)
{
CurrentCanvas.AnnotateUrl(size.Width, size.Height, url, description);
}
public void DrawSectionLink(Size size, string sectionName, string? description)
{
CurrentCanvas.AnnotateDestinationLink(size.Width, size.Height, sectionName, description);
}
public void DrawSection(string sectionName)
{
CurrentCanvas.AnnotateDestination(sectionName);
}
private int CurrentSemanticNodeId { get; set; } = 0;
public int GetSemanticNodeId()
{
return CurrentSemanticNodeId;
}
public void SetSemanticNodeId(int nodeId)
{
CurrentSemanticNodeId = nodeId;
foreach (var canvas in ZIndexCanvases)
canvas.Value.Canvas.SetSemanticNodeId(nodeId);
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Drawing/Exceptions/DocumentComposeException.cs
================================================
using System;
namespace QuestPDF.Drawing.Exceptions
{
public sealed class DocumentComposeException : Exception
{
internal DocumentComposeException(string message) : base(message)
{
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs
================================================
using System;
namespace QuestPDF.Drawing.Exceptions
{
public sealed class DocumentDrawingException : Exception
{
internal DocumentDrawingException(string message) : base(message)
{
}
internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
{
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/Exceptions/DocumentLayoutException.cs
================================================
using System;
namespace QuestPDF.Drawing.Exceptions
{
public sealed class DocumentLayoutException : Exception
{
internal DocumentLayoutException(string message) : base(message)
{
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/Exceptions/InitializationException.cs
================================================
using System;
namespace QuestPDF.Drawing.Exceptions
{
public sealed class InitializationException : Exception
{
internal InitializationException(string message) : base(message)
{
}
internal InitializationException(string message, Exception inner) : base(message, inner)
{
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/FontManager.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
using QuestPDF.Skia.Text;
namespace QuestPDF.Drawing
{
///
/// By default, the library searches all fonts available in the runtime environment.
/// This may work well on the development environment but may fail in the cloud where fonts are usually not installed.
/// It is safest deploy font files along with the application and then register them using this class.
///
public static class FontManager
{
internal static SkTypefaceProvider TypefaceProvider { get; } = new();
internal static SkFontManager CurrentFontManager => Settings.UseEnvironmentFonts ? SkFontManager.Global : SkFontManager.Local;
static FontManager()
{
SkNativeDependencyCompatibilityChecker.Test();
RegisterLibraryDefaultFonts();
}
[Obsolete("Since version 2022.8 this method has been renamed. Please use the RegisterFontWithCustomName method.")]
[ExcludeFromCodeCoverage]
public static void RegisterFontType(string fontName, Stream stream)
{
RegisterFontWithCustomName(fontName, stream);
}
///
/// Registers a TrueType font from a stream under the provided custom .
/// Refer to this font by using the same name as a font family in the API later on.
/// Learn more
///
public static void RegisterFontWithCustomName(string fontName, Stream stream)
{
using var fontData = SkData.FromStream(stream);
TypefaceProvider.AddTypefaceFromData(fontData);
TypefaceProvider.AddTypefaceFromData(fontData, fontName);
}
///
/// Registers a TrueType font from a stream. The font family name and all related attributes are detected automatically.
/// Learn more
///
public static void RegisterFont(Stream stream)
{
using var fontData = SkData.FromStream(stream);
TypefaceProvider.AddTypefaceFromData(fontData);
}
///
/// Registers a TrueType font from an embedded resource. The font family name and all related attributes are detected automatically.
/// Learn more
///
/// Path to the embedded resource (the case-sensitive name of the manifest resource being requested).
public static void RegisterFontFromEmbeddedResource(string pathName)
{
using var stream = Assembly.GetCallingAssembly().GetManifestResourceStream(pathName);
if (stream == null)
throw new ArgumentException($"Cannot load font file from an embedded resource. Please make sure that the resource is available or the path is correct: {pathName}");
RegisterFont(stream);
}
private static void RegisterLibraryDefaultFonts()
{
var fontFilePaths = SearchFontFiles();
foreach (var fileName in fontFilePaths)
{
try
{
using var fontFileStream = File.OpenRead(fileName);
RegisterFont(fontFileStream);
}
catch
{
}
}
ICollection SearchFontFiles()
{
const int maxFilesToScan = 100_000;
var applicationFiles = Settings
.FontDiscoveryPaths
.Where(Directory.Exists)
.Select(TryEnumerateFiles)
.SelectMany(file => file)
.Take(maxFilesToScan)
.ToList();
if (applicationFiles.Count == maxFilesToScan)
throw new InvalidOperationException($"The library has reached the limit of {maxFilesToScan} files to scan for font files. Please adjust the {nameof(Settings.FontDiscoveryPaths)} collection to include only the necessary directories. The reason of this exception is to prevent scanning too many files and avoid performance issues on the application startup.");
var supportedFontExtensions = new[] { ".ttf", ".otf", ".ttc", ".pfb" };
return applicationFiles
.Where(x => supportedFontExtensions.Contains(Path.GetExtension(x).ToLowerInvariant()))
.ToList();
ICollection TryEnumerateFiles(string path)
{
try
{
return Directory
.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)
.Take(maxFilesToScan)
.ToArray();
}
catch
{
return Array.Empty();
}
}
}
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/Proxy/ElementProxy.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class ElementProxy : ContainerElement
{
}
}
================================================
FILE: Source/QuestPDF/Drawing/Proxy/LayoutDebugging.cs
================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy;
internal static class LayoutDebugging
{
internal static SpacePlan TryMeasureWithOverflow(this Element element, Size availableSpace)
{
return TryVerticalOverflow()
?? TryHorizontalOverflow()
?? TryUnconstrainedOverflow()
?? SpacePlan.Wrap("Extending the available space does not allow the child to fit on the page.");
SpacePlan? TryOverflow(Size targetSpace)
{
var contentSize = element.Measure(targetSpace);
return contentSize.Type == SpacePlanType.Wrap ? null : contentSize;
}
SpacePlan? TryVerticalOverflow()
{
var overflowSpace = new Size(availableSpace.Width, Size.Infinity);
return TryOverflow(overflowSpace);
}
SpacePlan? TryHorizontalOverflow()
{
var overflowSpace = new Size(Size.Infinity, availableSpace.Height);
return TryOverflow(overflowSpace);
}
SpacePlan? TryUnconstrainedOverflow()
{
var overflowSpace = new Size(Size.Infinity, Size.Infinity);
return TryOverflow(overflowSpace);
}
}
public static void ApplyLayoutOverflowDetection(this Element container)
{
container.VisitChildren(x =>
{
x.CreateProxy(y => y is ElementProxy ? y : new OverflowDebuggingProxy(y));
});
}
public static void TryToFixTheLayoutOverflowIssue(this TreeNode hierarchyRoot)
{
Traverse(hierarchyRoot);
void Traverse(TreeNode element)
{
if (element.Value.Child is DebugPointer or SourceCodePointer or Container)
{
Traverse(element.Children.First());
return;
}
if (element.Value.AvailableSpace is null)
return;
// element was not part of the current layout measurement,
// it could not impact the process
if (element.Value.SpacePlan is null)
return;
// element is empty,
// it could not impact the process
if (element.Value.SpacePlan?.Type is SpacePlanType.Empty)
return;
// element renders fully,
// it could not impact the process
if (element.Value.SpacePlan?.Type is SpacePlanType.FullRender)
return;
// when the current element is partially rendering, it likely has no issues,
// however, in certain cases, it may contain a child that is a root cause
if (element.Value.SpacePlan?.Type is SpacePlanType.PartialRender)
{
foreach (var child in element.Children)
Traverse(child);
return;
}
// all the code below relates to element that is wrapping,
// it could be a root cause, or contain a child (even deeply nested) that is the root cause
// strategy:
// the current element does not contain any wrapping children, no obvious root causes,
// if it renders at least partially with extended space, it is a layout root cause
if (element.Children.All(x => x.Value.SpacePlan?.Type is not SpacePlanType.Wrap) && MeasureElementWithExtendedSpace() is not SpacePlanType.Wrap)
{
element.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
return;
}
// strategy:
// the current element contains wrapping children, they are likely the root cause,
// traverse them and attempt to fix them
foreach (var child in element.Children.Where(x => x.Value.SpacePlan?.Type is SpacePlanType.Wrap).ToList())
Traverse(child);
// check if fixing wrapping children resolved the issue under original constraints
if (MeasureElementWithOriginalSpace() is not SpacePlanType.Wrap)
return;
// fixing wrapping children was not sufficient under original constraints;
// if this element fits with extended space, it is also a root cause
if (MeasureElementWithExtendedSpace() is not SpacePlanType.Wrap)
{
element.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
return;
}
// strategy:
// the current element has layout issues but no obvious/trivial root causes,
// possibly the problem is in nested children of partial rendering children
foreach (var child in element.Children.Where(x => x.Value.SpacePlan?.Type is SpacePlanType.PartialRender).ToList())
Traverse(child);
// check if fixing partial children resolved the issue under original constraints
if (MeasureElementWithOriginalSpace() is not SpacePlanType.Wrap)
return;
// none of the attempts above have fixed the layout issue,
// the element itself is the root cause
element.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
SpacePlanType MeasureElementWithExtendedSpace()
{
return element.Value.TryMeasureWithOverflow(element.Value.AvailableSpace!.Value).Type;
}
SpacePlanType MeasureElementWithOriginalSpace()
{
return element.Value.Measure(element.Value.AvailableSpace!.Value).Type;
}
}
}
public static void RemoveExistingProxies(this Element content)
{
content.RemoveExistingProxiesOfType();
}
public static void RemoveExistingProxiesOfType(this Element content) where TProxy : ElementProxy
{
content.VisitChildren(x =>
{
x.CreateProxy(y =>
{
if (y is not TProxy proxy)
return y;
(proxy as IDisposable)?.Dispose();
return proxy.Child;
});
});
}
public static void StopMeasuring(this TreeNode parent)
{
parent.Value.StopMeasuring();
foreach (var child in parent.Children)
StopMeasuring(child);
}
public static IEnumerable> FindLayoutOverflowVisualizationNodes(this TreeNode rootNode)
{
var result = new List>();
Traverse(rootNode);
return result;
void Traverse(TreeNode node)
{
if (node.Value.Child is LayoutOverflowVisualization)
result.Add(node);
foreach (var child in node.Children)
Traverse(child);
}
}
public static string FormatAncestors(this IEnumerable ancestors)
{
var result = new StringBuilder();
foreach (var ancestor in ancestors)
Format(ancestor);
return result.ToString();
void Format(Element node)
{
if (node is DebugPointer debugPointer)
{
result.AppendLine($"-> {debugPointer.Label}");
}
else if (node is SourceCodePointer sourceCodePointer)
{
result.AppendLine($"-> In method: {sourceCodePointer.MethodName}");
result.AppendLine($" Called from: {sourceCodePointer.CalledFrom}");
result.AppendLine($" Source path: {sourceCodePointer.FilePath}");
result.AppendLine($" Line number: {sourceCodePointer.LineNumber}");
}
else
{
}
result.AppendLine();
}
}
public static string FormatLayoutSubtree(this TreeNode root)
{
var indentationCache = Enumerable.Range(0, 128).Select(x => x * 3).Select(x => new string(' ', x)).ToArray();
var indentationLevel = 0;
var result = new StringBuilder();
Traverse(root);
return result.ToString();
void Traverse(TreeNode parent)
{
var proxy = parent.Value;
if (proxy.Child is Container)
{
Traverse(parent.Children.First());
return;
}
var indent = indentationCache[indentationLevel];
foreach (var content in Format(proxy))
result.AppendLine($"{indent}{content}");
result.AppendLine();
result.AppendLine();
if (proxy.AvailableSpace is null || proxy.SpacePlan is null)
return;
indentationLevel++;
foreach (var child in parent.Children)
Traverse(child);
indentationLevel--;
}
static IEnumerable Format(OverflowDebuggingProxy proxy)
{
var child = proxy.Child;
if (child is LayoutOverflowVisualization layoutOverflowVisualization)
child = layoutOverflowVisualization.Child;
var title = GetTitle();
yield return title;
yield return new string('=', title.Length + 1);
if (proxy is { AvailableSpace: not null, SpacePlan: not null })
{
yield return $"Available Space: {proxy.AvailableSpace}";
yield return $"Space Plan: {proxy.SpacePlan}";
if (proxy.SpacePlan?.Type == SpacePlanType.Wrap)
yield return "Wrap Reason: " + (proxy.SpacePlan?.WrapReason ?? "Unknown");
yield return new string('-', title.Length + 1);
}
foreach (var configuration in child.GetElementConfiguration())
yield return $"{configuration.Property}: {configuration.Value}";
string GetTitle()
{
var elementName = child.GetType().Name;
if (proxy.Child is LayoutOverflowVisualization)
return $"🚨 {elementName} 🚨";
var indicator = proxy.SpacePlan?.Type switch
{
SpacePlanType.Wrap => "🔴",
SpacePlanType.PartialRender => "🟡",
SpacePlanType.FullRender => "🟢",
SpacePlanType.Empty => "🟢",
_ => "⚪️"
};
return $"{indicator} {elementName}";
}
}
}
public static IEnumerable<(string Property, string Value)> GetElementConfiguration(this IElement element)
{
return element
.GetType()
.GetProperties()
.Select(x => new
{
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.Select(x => (x.Property, x.Value?.ToString() ?? "-"));
}
public const string LayoutVisualizationLegend =
"Legend: \n" +
"🚨 - Element that is likely the root cause of the layout issue based on library heuristics and prediction. \n" +
"🔴 - Element that cannot be drawn due to the provided layout constraints. This element likely causes the layout issue, or one of its descendant children is responsible for the problem. \n" +
"🟡 - Element that can be partially drawn on the page and will also be rendered on the consecutive page. In more complex layouts, this element may also cause issues or contain a child that is the actual root cause.\n" +
"🟢 - Element that is successfully and completely drawn on the page.\n" +
"⚪️ - Element that has not been drawn on the faulty page. Its children are omitted.\n";
}
================================================
FILE: Source/QuestPDF/Drawing/Proxy/LayoutOverflowVisualization.cs
================================================
using System;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Drawing.Proxy;
internal sealed class LayoutOverflowVisualization : ElementProxy, IContentDirectionAware
{
private const float BorderThickness = 1.5f;
private readonly Color LineColor = Colors.Red.Medium;
private readonly Color AvailableAreaColor = Colors.Green.Medium;
private const byte AreaOpacity = 64;
public ContentDirection ContentDirection { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
if (Size.Equal(availableSpace, Size.Zero))
return SpacePlan.Wrap("There is no available space.");
var childSize = base.Measure(availableSpace);
if (childSize.Type == SpacePlanType.FullRender)
return childSize;
var minimalSize = Child.TryMeasureWithOverflow(availableSpace);
if (minimalSize.Type is SpacePlanType.Wrap)
return minimalSize;
var width = Math.Min(availableSpace.Width, minimalSize.Width);
var height = Math.Min(availableSpace.Height, minimalSize.Height);
return new SpacePlan(minimalSize.Type, width, height);
}
internal override void Draw(Size availableSpace)
{
// measure content area
var childSize = base.Measure(availableSpace);
if (childSize.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
{
Child?.Draw(availableSpace);
return;
}
Canvas = Child.Canvas;
// check overflow area
var contentArea = Child.TryMeasureWithOverflow(availableSpace);
var contentSize = contentArea.Type is SpacePlanType.Wrap
? Size.Max
: contentArea;
// draw content
var translate = ContentDirection == ContentDirection.RightToLeft
? new Position(availableSpace.Width - contentSize.Width, 0)
: Position.Zero;
Canvas.Translate(translate);
Child?.Draw(contentSize);
Canvas.Translate(translate.Reverse());
// draw overflow area
var overflowTranslate = ContentDirection == ContentDirection.RightToLeft ? new Position(availableSpace.Width, 0) : Position.Zero;
var overflowScale = ContentDirection == ContentDirection.RightToLeft ? -1 : 1;
Canvas.Translate(overflowTranslate);
Canvas.Scale(overflowScale, 1);
DrawOverflowArea(availableSpace, contentSize);
Canvas.Scale(overflowScale, 1);
Canvas.Translate(overflowTranslate.Reverse());
}
private void DrawOverflowArea(Size availableSpace, Size contentSize)
{
using var availableSpacePaint = new SkPaint();
availableSpacePaint.SetSolidColor(AvailableAreaColor.WithAlpha(AreaOpacity));
Canvas.DrawRectangle(Position.Zero, availableSpace, availableSpacePaint);
Canvas.Save();
Canvas.ClipOverflowArea(new SkRect(0, 0, availableSpace.Width, availableSpace.Height), new SkRect(0, 0, contentSize.Width, contentSize.Height));
Canvas.DrawOverflowArea(new SkRect(0, 0, contentSize.Width, contentSize.Height));
Canvas.Restore();
using var borderPaint = new SkPaint();
borderPaint.SetStroke(BorderThickness);
borderPaint.SetSolidColor(LineColor);
Canvas.DrawRectangle(Position.Zero, contentSize, borderPaint);
}
}
================================================
FILE: Source/QuestPDF/Drawing/Proxy/LayoutProxy.cs
================================================
using System.Collections.Generic;
using QuestPDF.Companion;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Elements;
using QuestPDF.Elements.Text;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using Image = QuestPDF.Elements.Image;
using SvgImage = QuestPDF.Elements.SvgImage;
namespace QuestPDF.Drawing.Proxy;
internal sealed class LayoutProxy : ElementProxy
{
public List Snapshots { get; } = new();
public List LayoutErrorMeasurements { get; } = new();
public LayoutProxy(Element child)
{
Child = child;
}
internal override void Draw(Size availableSpace)
{
var size = ProvideIntrinsicSize() ? Child.Measure(availableSpace) : availableSpace;
base.Draw(availableSpace);
if (!Canvas.Is())
return;
var matrix = Canvas.GetCurrentMatrix();
Snapshots.Add(new CompanionCommands.UpdateDocumentStructure.PageLocation
{
PageNumber = PageContext.CurrentPage,
Left = matrix.TranslateX,
Top = matrix.TranslateY,
Right = matrix.TranslateX + size.Width,
Bottom = matrix.TranslateY + size.Height
});
bool ProvideIntrinsicSize()
{
// Image or DynamicImage or SvgImage or DynamicSvgImage should be excluded
// They rely on the AspectRation component to provide true intrinsic size
return Child is TextBlock or AspectRatio or Unconstrained or SemanticTag or ArtifactTag;
}
}
internal void CaptureLayoutErrorMeasurement()
{
var child = Child;
while (true)
{
if (child is OverflowDebuggingProxy overflowDebuggingProxy)
{
if (overflowDebuggingProxy.AvailableSpace == null || overflowDebuggingProxy.SpacePlan == null)
break;
LayoutErrorMeasurements.Add(new CompanionCommands.UpdateDocumentStructure.LayoutErrorMeasurement
{
PageNumber = PageContext.CurrentPage,
AvailableSpace = new CompanionCommands.ElementSize
{
Width = overflowDebuggingProxy.AvailableSpace.Value.Width,
Height = overflowDebuggingProxy.AvailableSpace.Value.Height
},
MeasurementSize = new CompanionCommands.ElementSize
{
Width = overflowDebuggingProxy.SpacePlan.Value.Width,
Height = overflowDebuggingProxy.SpacePlan.Value.Height
},
SpacePlanType = overflowDebuggingProxy.SpacePlan?.Type,
WrapReason = overflowDebuggingProxy.SpacePlan?.WrapReason,
IsLayoutErrorRootCause = overflowDebuggingProxy.Child.GetType() == typeof(LayoutOverflowVisualization)
});
}
if (child is not ElementProxy proxy)
break;
child = proxy.Child;
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/Proxy/OverflowDebuggingProxy.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy;
internal sealed class OverflowDebuggingProxy : ElementProxy
{
public bool IsMeasuring { get; private set; } = true;
public Size? AvailableSpace { get; private set; }
public SpacePlan? SpacePlan { get; private set; }
public OverflowDebuggingProxy(Element child)
{
Child = child;
}
internal override SpacePlan Measure(Size availableSpace)
{
var spacePlan = Child.Measure(availableSpace);
if (IsMeasuring && !Size.Equal(availableSpace, Size.Zero))
{
AvailableSpace = availableSpace;
SpacePlan = spacePlan;
}
return spacePlan;
}
public void StopMeasuring()
{
IsMeasuring = false;
}
}
================================================
FILE: Source/QuestPDF/Drawing/Proxy/SnapshotCacheRecorderProxy.cs
================================================
using System;
using System.Collections.Generic;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Drawing.Proxy;
internal sealed class SnapshotCacheRecorderProxy : ElementProxy, IDisposable
{
private ProxyDrawingCanvas RecorderCanvas { get; } = new();
private Dictionary<(int pageNumber, float availableWidth, float availableHeight), SpacePlan> MeasureCache { get; } = new();
private Dictionary DrawCache { get; } = new();
~SnapshotCacheRecorderProxy()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
RecorderCanvas?.Dispose();
foreach (var cacheValue in DrawCache.Values)
cacheValue.Dispose();
GC.SuppressFinalize(this);
}
public SnapshotCacheRecorderProxy(Element child)
{
Child = child;
}
private void Initialize()
{
if (Child.Canvas == RecorderCanvas)
return;
Child.VisitChildren(x => x.Canvas = RecorderCanvas);
}
internal override SpacePlan Measure(Size availableSpace)
{
Initialize();
var cacheItem = (PageContext.CurrentPage, availableSpace.Width, availableSpace.Height);
if (MeasureCache.TryGetValue(cacheItem, out var measurement))
return measurement;
RecorderCanvas.Target = new DiscardDrawingCanvas();
var result = base.Measure(availableSpace);
RecorderCanvas.Target = null;
MeasureCache[cacheItem] = result;
return result;
}
internal override void Draw(Size availableSpace)
{
if (DrawCache.TryGetValue(PageContext.CurrentPage, out var snapshot))
{
Canvas.DrawSnapshot(snapshot);
snapshot.Dispose();
DrawCache.Remove(PageContext.CurrentPage);
return;
}
using var skiaCanvas = new SkiaDrawingCanvas(Size.Max.Width, Size.Max.Height);
RecorderCanvas.Target = skiaCanvas;
RecorderCanvas.SetZIndex(Canvas.GetZIndex());
RecorderCanvas.SetMatrix(Canvas.GetCurrentMatrix());
base.Draw(availableSpace);
DrawCache[PageContext.CurrentPage] = skiaCanvas.GetSnapshot();
RecorderCanvas.Target = null;
}
}
================================================
FILE: Source/QuestPDF/Drawing/Proxy/TreeTraversal.cs
================================================
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy;
internal sealed class TreeNode
{
public T Value { get; }
public TreeNode? Parent { get; set; }
public ICollection> Children { get; } = new List>();
public TreeNode(T Value)
{
this.Value = Value;
}
}
internal static class TreeTraversal
{
public static IEnumerable> ExtractElementsOfType(this Element element) where T : Element
{
if (element is T proxy)
{
var result = new TreeNode(proxy);
foreach (var treeNode in proxy.GetChildren().SelectMany(ExtractElementsOfType))
{
result.Children.Add(treeNode);
treeNode.Parent = result;
}
yield return result;
}
else
{
foreach (var treeNode in element.GetChildren().SelectMany(ExtractElementsOfType))
yield return treeNode;
}
}
public static IEnumerable> Flatten(this TreeNode element) where T : Element
{
yield return element;
foreach (var child in element.Children)
foreach (var innerChild in Flatten(child))
yield return innerChild;
}
public static IEnumerable> ExtractAncestors(this TreeNode node)
{
while (true)
{
node = node.Parent;
if (node is null)
yield break;
yield return node;
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/SemanticTreeManager.cs
================================================
using System;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing;
internal class SemanticTreeNode
{
public int NodeId { get; set; }
public string Type { get; set; } = "";
public string? Alt { get; set; }
public string? Lang { get; set; }
public IList Children { get; } = [];
public ICollection Attributes { get; } = [];
public class Attribute
{
public string Owner { get; set; }
public string Name { get; set; }
public object Value { get; set; }
}
}
class SemanticTreeManager
{
private int CurrentNodeId { get; set; }
private SemanticTreeNode? Root { get; set; }
private Stack Stack { get; set; } = [];
public SemanticTreeManager()
{
PopulateWithTopLevelNode();
}
private void PopulateWithTopLevelNode()
{
AddNode(new SemanticTreeNode
{
NodeId = GetNextNodeId(),
Type = "Document"
});
}
public int GetNextNodeId()
{
CurrentNodeId++;
return CurrentNodeId;
}
public void AddNode(SemanticTreeNode node)
{
if (Root == null)
{
Root = node;
Stack.Push(node);
return;
}
Stack.Peek()?.Children.Add(node);
}
public void PushOnStack(SemanticTreeNode node)
{
Stack.Push(node);
}
public void PopStack()
{
Stack.Pop();
}
public SemanticTreeNode PeekStack()
{
return Stack.Peek();
}
public void Reset()
{
CurrentNodeId = 0;
Root = null;
Stack.Clear();
}
public SemanticTreeNode? GetSemanticTree()
{
return Root;
}
#region Artifacts
private int ArtifactNestingLevel { get; set; } = 0;
public void BeginArtifactContent()
{
ArtifactNestingLevel++;
}
public void EndArtifactContent()
{
ArtifactNestingLevel--;
}
public bool IsCurrentContentArtifact()
{
return ArtifactNestingLevel > 0;
}
#endregion
#region State
public class StateSnapshot
{
internal int CurrentNodeId { get; init; }
}
public StateSnapshot GetState()
{
return new StateSnapshot
{
CurrentNodeId = CurrentNodeId
};
}
public void SetState(StateSnapshot state)
{
CurrentNodeId = state.CurrentNodeId;
}
#endregion
}
class SemanticTreeSnapshots(SemanticTreeManager? semanticTreeManager, IPageContext pageContext)
{
private IList Snapshots { get; } = [];
public SemanticTreeSnapshotScope? StartSemanticStateScope(int index)
{
if (semanticTreeManager == null)
return null;
var originalSemanticState = semanticTreeManager.GetState();
if (index >= Snapshots.Count)
{
Snapshots.Add(originalSemanticState);
}
else
{
var snapshot = Snapshots[index];
semanticTreeManager.SetState(snapshot);
}
return new SemanticTreeSnapshotScope(() =>
{
if (pageContext.IsInitialRenderingPhase)
return;
semanticTreeManager.SetState(originalSemanticState);
});
}
public class SemanticTreeSnapshotScope(Action resetState) : IDisposable
{
public void Dispose()
{
resetState();
GC.SuppressFinalize(this);
}
}
}
internal readonly ref struct SemanticScope : IDisposable
{
private IDrawingCanvas DrawingCanvas { get; }
private int OriginalSemanticNodeId { get; }
public SemanticScope(IDrawingCanvas drawingCanvas, int nodeId)
{
DrawingCanvas = drawingCanvas;
OriginalSemanticNodeId = drawingCanvas.GetSemanticNodeId();
DrawingCanvas.SetSemanticNodeId(nodeId);
}
public void Dispose()
{
DrawingCanvas.SetSemanticNodeId(OriginalSemanticNodeId);
}
}
internal static class SemanticCanvasExtensions
{
public static SemanticScope StartSemanticScopeWithNodeId(this IDrawingCanvas canvas, int nodeId)
{
return new SemanticScope(canvas, nodeId);
}
}
================================================
FILE: Source/QuestPDF/Drawing/SpacePlan.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing
{
internal readonly struct SpacePlan
{
public readonly SpacePlanType Type;
public readonly float Width;
public readonly float Height;
public readonly string? WrapReason;
internal SpacePlan(SpacePlanType type, float width, float height, string? wrapReason = null)
{
Type = type;
Width = width;
Height = height;
WrapReason = wrapReason;
}
internal static SpacePlan Empty() => new(SpacePlanType.Empty, 0, 0);
internal static SpacePlan Wrap(string reason) => new(SpacePlanType.Wrap, 0, 0, reason);
internal static SpacePlan PartialRender(float width, float height) => new(SpacePlanType.PartialRender, width, height);
internal static SpacePlan PartialRender(Size size) => PartialRender(size.Width, size.Height);
internal static SpacePlan FullRender(float width, float height) => new(SpacePlanType.FullRender, width, height);
internal static SpacePlan FullRender(Size size) => FullRender(size.Width, size.Height);
public override string ToString()
{
if (Type == SpacePlanType.Wrap)
return Type.ToString();
return $"{Type} (Width: {Width:N3}, Height: {Height:N3})";
}
public static implicit operator Size(SpacePlan spacePlan)
{
return new Size(spacePlan.Width, spacePlan.Height);
}
}
}
================================================
FILE: Source/QuestPDF/Drawing/SpacePlanType.cs
================================================
namespace QuestPDF.Drawing
{
internal enum SpacePlanType
{
Empty,
Wrap,
PartialRender,
FullRender
}
}
================================================
FILE: Source/QuestPDF/Elements/Alignment.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Alignment : ContainerElement
{
public VerticalAlignment? Vertical { get; set; }
public HorizontalAlignment? Horizontal { get; set; }
internal override void Draw(Size availableSpace)
{
var childMeasurement = base.Measure(availableSpace);
if (childMeasurement.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return;
var childSize = new Size(
Horizontal.HasValue ? childMeasurement.Width : availableSpace.Width,
Vertical.HasValue ? childMeasurement.Height : availableSpace.Height);
var top = GetTopOffset(availableSpace, childSize);
var left = GetLeftOffset(availableSpace, childSize);
Canvas.Translate(new Position(left, top));
base.Draw(childSize);
Canvas.Translate(new Position(-left, -top));
}
private float GetTopOffset(Size availableSpace, Size childSize)
{
var difference = availableSpace.Height - childSize.Height;
return Vertical switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Middle => difference / 2,
VerticalAlignment.Bottom => difference,
_ => 0
};
}
private float GetLeftOffset(Size availableSpace, Size childSize)
{
var difference = availableSpace.Width - childSize.Width;
return Horizontal switch
{
HorizontalAlignment.Left => 0,
HorizontalAlignment.Center => difference / 2,
HorizontalAlignment.Right => difference,
_ => 0
};
}
internal override string? GetCompanionHint() => $"{Vertical} {Horizontal}";
}
}
================================================
FILE: Source/QuestPDF/Elements/ArtifactTag.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements;
internal class ArtifactTag : ContainerElement, ISemanticAware
{
public SemanticTreeManager? SemanticTreeManager { get; set; }
public int Id { get; set; }
internal override void Draw(Size availableSpace)
{
if (SemanticTreeManager == null)
{
base.Draw(availableSpace);
return;
}
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(Id);
SemanticTreeManager.BeginArtifactContent();
Child?.Draw(availableSpace);
SemanticTreeManager.EndArtifactContent();
}
}
================================================
FILE: Source/QuestPDF/Elements/AspectRatio.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class AspectRatio : ContainerElement, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
public float Ratio { get; set; }
public AspectRatioOption Option { get; set; } = AspectRatioOption.FitWidth;
internal override SpacePlan Measure(Size availableSpace)
{
if (Ratio == 0)
return SpacePlan.FullRender(0, 0);
if (Child.IsEmpty())
return SpacePlan.Empty();
if (availableSpace.IsCloseToZero())
return SpacePlan.Wrap("The available space is zero.");
var targetSize = GetTargetSize(availableSpace);
if (targetSize.Height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap("To preserve the target aspect ratio, the content requires more vertical space than available.");
if (targetSize.Width > availableSpace.Width + Size.Epsilon)
return SpacePlan.Wrap("To preserve the target aspect ratio, the content requires more horizontal space than available.");
var childSize = base.Measure(targetSize);
if (childSize.Type == SpacePlanType.Wrap)
return childSize;
if (childSize.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(targetSize);
if (childSize.Type == SpacePlanType.FullRender)
return SpacePlan.FullRender(targetSize);
throw new NotSupportedException();
}
internal override void Draw(Size availableSpace)
{
var size = GetTargetSize(availableSpace);
var offset = ContentDirection == ContentDirection.LeftToRight
? Position.Zero
: new Position(availableSpace.Width - size.Width, 0);
Canvas.Translate(offset);
base.Draw(size);
Canvas.Translate(offset.Reverse());
}
private Size GetTargetSize(Size availableSpace)
{
if (Ratio == 0)
return availableSpace;
var spaceRatio = availableSpace.Width / availableSpace.Height;
var fitHeight = new Size(availableSpace.Height * Ratio, availableSpace.Height) ;
var fitWidth = new Size(availableSpace.Width, availableSpace.Width / Ratio);
return Option switch
{
AspectRatioOption.FitWidth => fitWidth,
AspectRatioOption.FitHeight => fitHeight,
AspectRatioOption.FitArea => Ratio < spaceRatio ? fitHeight : fitWidth,
_ => throw new ArgumentOutOfRangeException()
};
}
internal override string? GetCompanionHint() => $"{Option.ToString()} with ratio {Ratio:F1}";
}
}
================================================
FILE: Source/QuestPDF/Elements/Column.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class ColumnItemRenderingCommand
{
public Element Element { get; set; }
public SpacePlan Measurement { get; set; }
public Position Offset { get; set; }
}
internal sealed class Column : Element, IStateful
{
internal List Items { get; } = new();
internal float Spacing { get; set; }
internal override IEnumerable GetChildren()
{
return Items;
}
internal override void CreateProxy(Func create)
{
for (var i = 0; i < Items.Count; i++)
Items[i] = create(Items[i]);
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!Items.Any())
return SpacePlan.Empty();
if (CurrentRenderingIndex == Items.Count)
return SpacePlan.Empty();
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
var renderingCommands = PlanLayout(availableSpace);
if (!renderingCommands.Any())
return SpacePlan.Wrap("The available space is not sufficient for even partially rendering a single item.");
var width = renderingCommands.Max(x => x.Measurement.Width);
var height = renderingCommands.Last().Offset.Y + renderingCommands.Last().Measurement.Height;
var size = new Size(width, height);
if (width > availableSpace.Width + Size.Epsilon)
return SpacePlan.Wrap("The content requires more horizontal space than available.");
if (height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap("The content requires more vertical space than available.");
var totalRenderedItems = CurrentRenderingIndex + renderingCommands.Count(x => x.Measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender);
var willBeFullyRendered = totalRenderedItems == Items.Count;
return willBeFullyRendered
? SpacePlan.FullRender(size)
: SpacePlan.PartialRender(size);
}
internal override void Draw(Size availableSpace)
{
var renderingCommands = PlanLayout(availableSpace);
foreach (var command in renderingCommands)
{
var targetSize = new Size(availableSpace.Width, command.Measurement.Height);
Canvas.Translate(command.Offset);
command.Element.Draw(targetSize);
Canvas.Translate(command.Offset.Reverse());
}
var fullyRenderedItems = renderingCommands.Count(x => x.Measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender);
CurrentRenderingIndex += fullyRenderedItems;
}
private List PlanLayout(Size availableSpace)
{
var topOffset = 0f;
var commands = new List();
foreach (var item in Items.Skip(CurrentRenderingIndex))
{
var isFirstItem = commands.Count == 0;
var availableHeight = availableSpace.Height - topOffset;
if (availableHeight < -Size.Epsilon)
break;
availableHeight = Math.Max(0, availableHeight);
if (!isFirstItem)
availableHeight -= Spacing;
var allowOnlyZeroSpaceItems = availableHeight < Size.Epsilon;
var itemSpace = allowOnlyZeroSpaceItems
? Size.Zero
: new Size(availableSpace.Width, availableHeight);
var measurement = item.Measure(itemSpace);
if (measurement.Type == SpacePlanType.Wrap)
break;
var currentItemTookSpace = !Size.Equal(measurement, Size.Zero);
if (allowOnlyZeroSpaceItems && currentItemTookSpace)
break;
if (!isFirstItem && currentItemTookSpace)
topOffset += Spacing;
commands.Add(new ColumnItemRenderingCommand
{
Element = item,
Measurement = measurement,
Offset = new Position(0, topOffset)
});
if (measurement.Type == SpacePlanType.PartialRender)
break;
topOffset += measurement.Height;
}
return commands;
}
#region IStateful
internal int CurrentRenderingIndex { get; set; }
public void ResetState(bool hardReset = false) => CurrentRenderingIndex = 0;
public object GetState() => CurrentRenderingIndex;
public void SetState(object state) => CurrentRenderingIndex = (int) state;
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/Constrained.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Constrained : ContainerElement, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
public float? MinWidth { get; set; }
public float? MaxWidth { get; set; }
public float? MinHeight { get; set; }
public float? MaxHeight { get; set; }
public bool EnforceSizeWhenEmpty { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
if (MinWidth > MaxWidth)
return SpacePlan.Wrap($"The minimum width {MinWidth} is greater than the maximum width {MaxWidth}.");
if (MinHeight > MaxHeight)
return SpacePlan.Wrap($"The minimum height {MinHeight} is greater than the maximum height {MaxHeight}.");
if (!EnforceSizeWhenEmpty && Child.IsEmpty())
return SpacePlan.Empty();
if (MinWidth > availableSpace.Width + Size.Epsilon)
return SpacePlan.Wrap("The available horizontal space is less than the minimum width.");
if (MinHeight > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap("The available vertical space is less than the minimum height.");
var available = new Size(
Min(MaxWidth, availableSpace.Width),
Min(MaxHeight, availableSpace.Height));
var measurement = base.Measure(available);
if (measurement.Type == SpacePlanType.Wrap)
return measurement;
var actualSize = new Size(
Max(MinWidth, measurement.Width),
Max(MinHeight, measurement.Height));
if (measurement.Type == SpacePlanType.Empty)
return EnforceSizeWhenEmpty ? SpacePlan.FullRender(actualSize) : SpacePlan.Empty();
if (measurement.Type == SpacePlanType.FullRender)
return SpacePlan.FullRender(actualSize);
if (measurement.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(actualSize);
throw new NotSupportedException();
}
internal override void Draw(Size availableSpace)
{
var size = new Size(
Min(MaxWidth, availableSpace.Width),
Min(MaxHeight, availableSpace.Height));
var offset = ContentDirection == ContentDirection.LeftToRight
? Position.Zero
: new Position(availableSpace.Width - size.Width, 0);
Canvas.Translate(offset);
base.Draw(size);
Canvas.Translate(offset.Reverse());
}
private static float Min(float? x, float y)
{
return x.HasValue ? Math.Min(x.Value, y) : y;
}
private static float Max(float? x, float y)
{
return x.HasValue ? Math.Max(x.Value, y) : y;
}
internal override string? GetCompanionHint()
{
var width = FormatRange("W", MinWidth, MaxWidth);
var height = FormatRange("H", MinHeight, MaxHeight);
return string.Join(" ", width.Concat(height));
static IEnumerable FormatRange(string prefix, float? min, float? max)
{
if (!min.HasValue && !max.HasValue)
yield break;
if (min == max)
{
yield return $"{prefix}={min:F1}";
yield break;
}
if (min.HasValue)
yield return $"{prefix}≥{min:F1}";
if (max.HasValue)
yield return $"{prefix}≤{max:F1}";
}
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Container.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class Container : ContainerElement
{
internal Container()
{
}
}
}
================================================
FILE: Source/QuestPDF/Elements/ContentDirectionSetter.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class ContentDirectionSetter : ContainerElement
{
public ContentDirection ContentDirection { get; set; }
}
}
================================================
FILE: Source/QuestPDF/Elements/DebugArea.cs
================================================
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements
{
internal sealed class DebugArea : IComponent
{
public IElement? Child { get; set; }
public string Text { get; set; }
public Color Color { get; set; } = Colors.Red.Medium;
public void Compose(IContainer container)
{
var backgroundColor = Color.WithAlpha(64);
container
.Border(1)
.BorderColor(Color)
.Layers(layers =>
{
layers.PrimaryLayer().Element(Child);
layers.Layer().Background(backgroundColor);
layers
.Layer()
.ShowIf(!string.IsNullOrWhiteSpace(Text))
.AlignCenter()
.Shrink()
.Background(Colors.White)
.Padding(2)
.Text(Text)
.FontColor(Color)
.FontFamily(Fonts.Consolas)
.FontSize(8);
});
}
}
}
================================================
FILE: Source/QuestPDF/Elements/DebugPointer.cs
================================================
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal enum DocumentStructureTypes
{
Document,
Page,
Background,
Foreground,
Header,
Content,
Footer
}
internal enum DebugPointerType
{
DocumentStructure,
ElementStructure,
Component,
Section,
Dynamic,
UserDefined
}
internal sealed class DebugPointer : ContainerElement
{
public DebugPointerType Type { get; set; }
public string Label { get; set; }
internal override string? GetCompanionSearchableContent() => Label;
public DebugPointer()
{
}
public DebugPointer(DebugPointerType type, string label)
{
Type = type;
Label = label;
}
internal override IEnumerable>? GetCompanionProperties()
{
yield return new("Type", Type.ToString());
yield return new("Label", Label);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Decoration.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class DecorationElementLayout
{
public ItemCommand Before { get; set; }
public ItemCommand Content { get; set; }
public ItemCommand After { get; set; }
public struct ItemCommand
{
public Element Element;
public SpacePlan Measurement;
public Position Offset;
}
}
internal sealed class Decoration : Element, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
internal Element Before { get; set; } = new DebugPointer(DebugPointerType.ElementStructure, "Before");
internal Element Content { get; set; } = new DebugPointer(DebugPointerType.ElementStructure, "Content");
internal Element After { get; set; } = new DebugPointer(DebugPointerType.ElementStructure, "After");
internal override IEnumerable GetChildren()
{
yield return Before;
yield return Content;
yield return After;
}
internal override void CreateProxy(Func create)
{
Before = create(Before);
Content = create(Content);
After = create(After);
}
internal override SpacePlan Measure(Size availableSpace)
{
var layout = PlanLayout(availableSpace);
if (layout.Content.Measurement.Type == SpacePlanType.Empty)
return SpacePlan.Empty();
if (layout.Content.Measurement.Type == SpacePlanType.Wrap)
return SpacePlan.Wrap("The primary content does not fit on the page.");
if (layout.Before.Measurement.Type == SpacePlanType.Wrap)
return layout.Before.Measurement;
if (layout.After.Measurement.Type == SpacePlanType.Wrap)
return layout.After.Measurement;
var itemMeasurements = new[]
{
layout.Before.Measurement,
layout.Content.Measurement,
layout.After.Measurement
};
var width = itemMeasurements.Max(x => x.Width);
var height = itemMeasurements.Sum(x => x.Height);
var size = new Size(width, height);
if (width > availableSpace.Width + Size.Epsilon)
return SpacePlan.Wrap("The content slot requires more horizontal space than available.");
if (height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap("The content slot requires more vertical space than available.");
var willBeFullyRendered = itemMeasurements.All(x => x.Type is SpacePlanType.Empty or SpacePlanType.FullRender);
return willBeFullyRendered
? SpacePlan.FullRender(size)
: SpacePlan.PartialRender(size);
}
internal override void Draw(Size availableSpace)
{
var layout = PlanLayout(availableSpace);
var drawingCommands = new[]
{
layout.Before,
layout.Content,
layout.After
};
var width = drawingCommands.Max(x => x.Measurement.Width);
foreach (var command in drawingCommands)
{
var elementSize = new Size(width, command.Measurement.Height);
var offset = ContentDirection == ContentDirection.LeftToRight
? command.Offset
: new Position(availableSpace.Width - width, command.Offset.Y);
Canvas.Translate(offset);
command.Element.Draw(elementSize);
Canvas.Translate(offset.Reverse());
}
}
private DecorationElementLayout PlanLayout(Size availableSpace)
{
SpacePlan GetDecorationMeasurement(Element element)
{
var measurement = element.Measure(availableSpace);
if (measurement.Type is SpacePlanType.PartialRender or SpacePlanType.Wrap)
return SpacePlan.Wrap("Decoration slot (before or after) does not fit fully on the page.");
return measurement;
}
var beforeMeasurement = GetDecorationMeasurement(Before);
var afterMeasurement = GetDecorationMeasurement(After);
var contentSpace = new Size(availableSpace.Width, availableSpace.Height - beforeMeasurement.Height - afterMeasurement.Height);
var contentMeasurement = Content.Measure(contentSpace);
return new DecorationElementLayout
{
Before = new DecorationElementLayout.ItemCommand
{
Element = Before,
Measurement = beforeMeasurement,
Offset = Position.Zero
},
Content = new DecorationElementLayout.ItemCommand
{
Element = Content,
Measurement = contentMeasurement,
Offset = new Position(0, beforeMeasurement.Height)
},
After = new DecorationElementLayout.ItemCommand
{
Element = After,
Measurement = afterMeasurement,
Offset = new Position(0, beforeMeasurement.Height + contentMeasurement.Height)
},
};
}
}
}
================================================
FILE: Source/QuestPDF/Elements/DefaultTextStyle.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class DefaultTextStyle : ContainerElement
{
public TextStyle TextStyle { get; set; } = TextStyle.Default;
}
}
================================================
FILE: Source/QuestPDF/Elements/Dynamic.cs
================================================
using System;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class DynamicHost : Element, IStateful, IContentDirectionAware, ISemanticAware
{
public SemanticTreeManager? SemanticTreeManager { get; set; }
private SemanticTreeSnapshots? SemanticTreeSnapshots { get; set; }
private DynamicComponentProxy Child { get; }
private object InitialComponentState { get; set; }
internal TextStyle TextStyle { get; set; } = TextStyle.Default;
public ContentDirection ContentDirection { get; set; }
internal int? ImageTargetDpi { get; set; }
internal ImageCompressionQuality? ImageCompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
public DynamicHost(DynamicComponentProxy child)
{
Child = child;
InitialComponentState = Child.GetState();
}
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
if (availableSpace.IsCloseToZero())
return SpacePlan.PartialRender(Size.Zero);
var context = CreateContext(availableSpace);
var result = ComposeContent(context, acceptNewState: false);
var content = result.Content as Element ?? Empty.Instance;
var measurement = content.Measure(availableSpace);
context.DisposeCreatedElements();
content.ReleaseDisposableChildren();
if (measurement.Type is SpacePlanType.PartialRender or SpacePlanType.Wrap)
throw new DocumentLayoutException("Dynamic component generated content that does not fit on a single page.");
return result.HasMoreContent
? SpacePlan.PartialRender(measurement)
: SpacePlan.FullRender(measurement);
}
internal override void Draw(Size availableSpace)
{
SemanticTreeSnapshots ??= new SemanticTreeSnapshots(SemanticTreeManager, PageContext);
using var scope = SemanticTreeSnapshots.StartSemanticStateScope(RenderCount);
var context = CreateContext(availableSpace);
var composeResult = ComposeContent(context, acceptNewState: true);
var content = composeResult.Content as Element;
content?.Draw(availableSpace);
context.DisposeCreatedElements();
content.ReleaseDisposableChildren();
if (!composeResult.HasMoreContent)
IsRendered = true;
RenderCount++;
}
private DynamicContext CreateContext(Size availableSize)
{
return new DynamicContext
{
PageContext = PageContext,
Canvas = Canvas,
SemanticTreeManager = SemanticTreeManager,
TextStyle = TextStyle,
ContentDirection = ContentDirection,
ImageTargetDpi = ImageTargetDpi.Value,
ImageCompressionQuality = ImageCompressionQuality.Value,
UseOriginalImage = UseOriginalImage,
PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.IsInitialRenderingPhase ? int.MaxValue : PageContext.DocumentLength,
AvailableSize = availableSize
};
}
private DynamicComponentComposeResult ComposeContent(DynamicContext context, bool acceptNewState)
{
var componentState = Child.GetState();
var result = Child.Compose(context);
if (!acceptNewState)
Child.SetState(componentState);
return result;
}
#region IStateful
public struct DynamicState
{
public int RenderCount;
public bool IsRendered;
public object ChildState;
}
private int RenderCount { get; set; }
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false)
{
RenderCount = 0;
IsRendered = false;
Child.SetState(InitialComponentState);
}
public object GetState()
{
return new DynamicState
{
RenderCount = RenderCount,
IsRendered = IsRendered,
ChildState = Child.GetState()
};
}
public void SetState(object state)
{
var dynamicState = (DynamicState) state;
RenderCount = dynamicState.RenderCount;
IsRendered = dynamicState.IsRendered;
Child.SetState(dynamicState.ChildState);
}
#endregion
}
///
/// Stores all contextual information available for the dynamic component.
///
public sealed class DynamicContext
{
internal IPageContext PageContext { get; set; }
internal IDrawingCanvas Canvas { get; set; }
internal SemanticTreeManager? SemanticTreeManager { get; set; }
internal TextStyle TextStyle { get; set; }
internal ContentDirection ContentDirection { get; set; }
internal int ImageTargetDpi { get; set; }
internal ImageCompressionQuality ImageCompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
internal List CreatedElements { get; } = new();
///
/// Returns the number of the page being rendered at the moment.
///
public int PageNumber { get; internal set; }
///
/// Returns the total count of pages in the document.
///
///
///
/// Document rendering process is performed in two phases.
/// During the first phase, the value of this property is equal to int.MaxValue to indicate its unavailability.
///
/// Please note that using this property may result with unstable layouts and unpredicted behaviors, especially when generating conditional content of various sizes.
///
public int TotalPages { get; internal set; }
///
/// Returns the vertical and horizontal space, in points, available to the dynamic component.
///
public Size AvailableSize { get; internal set; }
///
/// Returns all page locations of the captured element.
///
public ICollection GetContentCapturedPositions(string id)
{
return PageContext.GetContentCapturedPositions(id);
}
///
/// Enables the creation of unattached layout structures and provides their size measurements.
///
/// The handler responsible for constructing the new layout structure.
/// A newly created content, with its physical size.
public IDynamicElement CreateElement(Action content)
{
var container = new DynamicElement();
CreatedElements.Add(container);
content(container);
if (SemanticTreeManager != null)
{
container.ApplySemanticParagraphs();
container.InjectSemanticTreeManager(SemanticTreeManager);
}
container.ApplyInheritedAndGlobalTexStyle(TextStyle);
container.ApplyContentDirection(ContentDirection);
container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality, UseOriginalImage);
container.InjectDependencies(PageContext, Canvas);
container.VisitChildren(x => (x as IStateful)?.ResetState());
container.Size = container.Measure(Size.Max);
return container;
}
internal void DisposeCreatedElements()
{
foreach (var element in CreatedElements)
element.ReleaseDisposableChildren();
CreatedElements.Clear();
}
}
///
/// Represents any unattached content element, created by the dynamic component.
///
public interface IDynamicElement : IElement
{
///
/// Specifies the vertical and horizontal size, measured in points, required by the element to be drawn completely, assuming infinite canvas.
///
Size Size { get; }
}
internal sealed class DynamicElement : ContainerElement, IDynamicElement
{
public Size Size { get; internal set; }
}
}
================================================
FILE: Source/QuestPDF/Elements/DynamicImage.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements
{
public sealed class GenerateDynamicImageDelegatePayload
{
public Size AvailableSpace { get; set; }
public ImageSize ImageSize { get; set; }
public int Dpi { get; set; }
}
///
/// Generates an image based on the given resolution.
///
/// Desired resolution of the image in pixels.
/// Desired resolution of the image in dots per inch.
/// An image in PNG, JPEG, or WEBP image format returned as byte array.
public delegate byte[]? GenerateDynamicImageDelegate(GenerateDynamicImageDelegatePayload payload);
internal sealed class DynamicImage : Element, IStateful, IDisposable
{
internal int? TargetDpi { get; set; }
internal ImageCompressionQuality? CompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
public GenerateDynamicImageDelegate? Source { get; set; }
private List<(Size Size, SkImage? Image)> Cache { get; } = new(1);
private float GenerationTime { get; set; }
private int DrawnImageSize { get; set; }
~DynamicImage()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
foreach (var cacheItem in Cache)
cacheItem.Image?.Dispose();
Cache.Clear();
GC.SuppressFinalize(this);
}
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
return SpacePlan.FullRender(availableSpace);
}
internal override void Draw(Size availableSpace)
{
var stopWatch = Stopwatch.StartNew();
var targetImage = Cache.FirstOrDefault(x => Size.Equal(x.Size, availableSpace)).Image;
if (targetImage == null)
{
targetImage = GetImage(availableSpace);
Cache.Add((availableSpace, targetImage));
}
if (targetImage != null)
Canvas.DrawImage(targetImage, availableSpace);
GenerationTime += (float) stopWatch.Elapsed.TotalMilliseconds;
DrawnImageSize += targetImage?.EncodedDataSize ?? 0;
IsRendered = true;
}
private SkImage? GetImage(Size availableSpace)
{
var dpi = TargetDpi ?? DocumentSettings.DefaultRasterDpi;
var sourcePayload = new GenerateDynamicImageDelegatePayload
{
AvailableSpace = availableSpace,
ImageSize = GetTargetResolution(availableSpace, dpi),
Dpi = dpi
};
var imageBytes = Source?.Invoke(sourcePayload);
if (imageBytes == null)
return null;
using var imageData = SkData.FromBinary(imageBytes);
var originalImage = SkImage.FromData(imageData);
if (UseOriginalImage)
return originalImage;
var compressedImage = originalImage.CompressImage(CompressionQuality.Value);
if (originalImage.EncodedDataSize > compressedImage.EncodedDataSize)
{
originalImage.Dispose();
return compressedImage;
}
else
{
compressedImage.Dispose();
return originalImage;
}
}
private static ImageSize GetTargetResolution(Size availableSize, int targetDpi)
{
var scalingFactor = targetDpi / (float)DocumentSettings.DefaultRasterDpi;
return new ImageSize(
(int)(availableSize.Width * scalingFactor),
(int)(availableSize.Height * scalingFactor)
);
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false) => IsRendered = false;
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
internal override string? GetCompanionHint()
{
var sizeKB = Math.Max(1, DrawnImageSize / 1024);
return $"{sizeKB}KB, generated in {GenerationTime:0.00}ms";
}
}
}
================================================
FILE: Source/QuestPDF/Elements/DynamicSvgImage.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements;
internal sealed class DynamicSvgImage : Element, IStateful, IDisposable
{
public GenerateDynamicSvgDelegate SvgSource { get; set; }
private List<(Size Size, SkSvgImage? Image)> Cache { get; } = new(1);
~DynamicSvgImage()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
foreach (var cacheItem in Cache)
cacheItem.Image?.Dispose();
Cache.Clear();
GC.SuppressFinalize(this);
}
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
return SpacePlan.FullRender(availableSpace);
}
internal override void Draw(Size availableSpace)
{
var targetImage = Cache.FirstOrDefault(x => Size.Equal(x.Size, availableSpace)).Image;
if (targetImage == null)
{
targetImage = GetImage(availableSpace);
Cache.Add((availableSpace, targetImage));
}
if (targetImage != null)
{
var (widthScale, heightScale) = targetImage.CalculateSpaceScale(availableSpace);
Canvas.Save();
Canvas.Scale(widthScale, heightScale);
Canvas.DrawSvg(targetImage, availableSpace);
Canvas.Restore();
}
IsRendered = true;
}
private SkSvgImage? GetImage(Size availableSpace)
{
var svg = SvgSource?.Invoke(availableSpace);
if (svg == null)
return null;
return new SkSvgImage(svg, SkResourceProvider.CurrentResourceProvider, FontManager.CurrentFontManager);
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false) => IsRendered = false;
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
}
================================================
FILE: Source/QuestPDF/Elements/ElementPositionLocator.cs
================================================
using System.Numerics;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ElementPositionLocator : ContainerElement
{
public string Id { get; set; }
internal override void Draw(Size availableSpace)
{
base.Draw(availableSpace);
var transform = Canvas.GetCurrentMatrix().ToMatrix4x4();
var scaleX = new Vector2(transform.M11, transform.M12).Length();
var scaleY = new Vector2(transform.M21, transform.M22).Length();
var actualPosition = Vector2.Transform(Vector2.Zero, transform);
var position = new PageElementLocation
{
Id = Id,
PageNumber = PageContext.CurrentPage,
Width = availableSpace.Width * scaleX,
Height = availableSpace.Height * scaleY,
X = actualPosition.X,
Y = actualPosition.Y,
Transform = transform
};
PageContext.CaptureContentPosition(position);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Empty.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Empty : Element
{
internal static Empty Instance { get; } = new();
internal override SpacePlan Measure(Size availableSpace)
{
return availableSpace.IsNegative()
? SpacePlan.Wrap("The available space is negative.")
: SpacePlan.FullRender(0, 0);
}
internal override void Draw(Size availableSpace)
{
}
}
}
================================================
FILE: Source/QuestPDF/Elements/EnsureSpace.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class EnsureSpace : ContainerElement, IStateful
{
public const float DefaultMinHeight = 150;
public float MinHeight { get; set; } = DefaultMinHeight;
internal override SpacePlan Measure(Size availableSpace)
{
var measurement = base.Measure(availableSpace);
if (IsFirstPageRendered)
return measurement;
if (measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
return measurement;
if (measurement.Type == SpacePlanType.PartialRender && MinHeight <= measurement.Height)
return measurement;
return SpacePlan.PartialRender(Size.Zero);
}
internal override void Draw(Size availableSpace)
{
if (IsFirstPageRendered)
{
base.Draw(availableSpace);
return;
}
var measurement = base.Measure(availableSpace);
if (measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
base.Draw(availableSpace);
if (measurement.Type is SpacePlanType.PartialRender && MinHeight <= measurement.Height)
base.Draw(availableSpace);
IsFirstPageRendered = true;
}
internal override string? GetCompanionHint() => $"at least {MinHeight}";
#region IStateful
private bool IsFirstPageRendered { get; set; }
public void ResetState(bool hardReset = false)
{
if (hardReset)
IsFirstPageRendered = false;
}
public object GetState() => IsFirstPageRendered;
public void SetState(object state) => IsFirstPageRendered = (bool) state;
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/Extend.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Extend : ContainerElement
{
public bool ExtendVertical { get; set; }
public bool ExtendHorizontal { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
var childSize = base.Measure(availableSpace);
if (childSize.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return childSize;
var targetSize = GetTargetSize(availableSpace, childSize);
if (childSize.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(targetSize);
if (childSize.Type == SpacePlanType.FullRender)
return SpacePlan.FullRender(targetSize);
throw new NotSupportedException();
}
private Size GetTargetSize(Size availableSpace, Size childSize)
{
return new Size(
ExtendHorizontal ? availableSpace.Width : childSize.Width,
ExtendVertical ? availableSpace.Height : childSize.Height);
}
internal override string? GetCompanionHint()
{
return (ExtendVertical, ExtendHorizontal) switch
{
(true, true) => "Both axes",
(true, false) => "Vertical axis",
(false, true) => "Horizontal axis",
(false, false) => null
};
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Grid.cs
================================================
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class GridElement
{
public int Columns { get; set; } = 1;
public Element? Child { get; set; }
}
internal sealed class Grid : IComponent
{
public const int DefaultColumnsCount = 12;
public List Children { get; } = new List();
public Queue ChildrenQueue { get; set; } = new Queue();
public int ColumnsCount { get; set; } = DefaultColumnsCount;
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public float VerticalSpacing { get; set; } = 0;
public float HorizontalSpacing { get; set; } = 0;
public void Compose(IContainer container)
{
ChildrenQueue = new Queue(Children);
container.Column(column =>
{
column.Spacing(VerticalSpacing);
while (ChildrenQueue.Any())
column.Item().Row(BuildRow);
});
}
IEnumerable GetRowElements()
{
var rowLength = 0;
while (ChildrenQueue.Any())
{
var element = ChildrenQueue.Peek();
if (rowLength + element.Columns > ColumnsCount)
break;
rowLength += element.Columns;
yield return ChildrenQueue.Dequeue();
}
}
void BuildRow(RowDescriptor row)
{
row.Spacing(HorizontalSpacing);
var elements = GetRowElements().ToList();
var columnsWidth = elements.Sum(x => x.Columns);
var emptySpace = ColumnsCount - columnsWidth;
var hasEmptySpace = emptySpace >= Size.Epsilon;
if (Alignment == HorizontalAlignment.Center)
emptySpace /= 2;
if (hasEmptySpace && Alignment != HorizontalAlignment.Left)
row.RelativeItem(emptySpace);
elements.ForEach(x => row.RelativeItem(x.Columns).Element(x.Child));
if (hasEmptySpace && Alignment != HorizontalAlignment.Right)
row.RelativeItem(emptySpace);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Hyperlink.cs
================================================
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Hyperlink : ContainerElement, IContentDirectionAware, ISemanticAware
{
public SemanticTreeManager? SemanticTreeManager { get; set; }
public ContentDirection ContentDirection { get; set; }
public string Url { get; set; } = "https://www.questpdf.com";
public string? Description { get; set; }
internal override void Draw(Size availableSpace)
{
if (SemanticTreeManager?.IsCurrentContentArtifact() ?? false)
{
base.Draw(availableSpace);
return;
}
var targetSize = base.Measure(availableSpace);
if (targetSize.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return;
var horizontalOffset = ContentDirection == ContentDirection.LeftToRight
? Position.Zero
: new Position(availableSpace.Width - targetSize.Width, 0);
Canvas.Translate(horizontalOffset);
Canvas.DrawHyperlink(availableSpace, Url, Description);
Canvas.Translate(horizontalOffset.Reverse());
base.Draw(availableSpace);
}
internal override string? GetCompanionHint() => Url;
internal override IEnumerable>? GetCompanionProperties()
{
yield return new("Url", Url);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Image.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements
{
internal sealed class Image : Element, IStateful, IDisposable
{
public Infrastructure.Image? DocumentImage { get; set; }
internal bool UseOriginalImage { get; set; }
internal int? TargetDpi { get; set; }
internal ImageCompressionQuality? CompressionQuality { get; set; }
private int DrawnImageSize { get; set; }
~Image()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
if (DocumentImage != null && !DocumentImage.IsShared)
DocumentImage?.Dispose();
GC.SuppressFinalize(this);
}
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
return SpacePlan.FullRender(Size.Zero);
}
internal override void Draw(Size availableSpace)
{
if (DocumentImage == null)
return;
if (IsRendered)
return;
var image = GetImageToDraw(availableSpace);
Canvas.DrawImage(image, availableSpace);
DrawnImageSize = Math.Max(DrawnImageSize, image.EncodedDataSize);
IsRendered = true;
}
private SkImage GetImageToDraw(Size availableSpace)
{
var originalImage = DocumentImage.SkImage;
if (UseOriginalImage)
return originalImage;
var request = new GetImageVersionRequest
{
Resolution = GetTargetResolution(DocumentImage.Size, availableSpace, TargetDpi.Value),
CompressionQuality = CompressionQuality.Value
};
var targetImage = DocumentImage.GetVersionOfSize(request);
return Helpers.Helpers.GetImageWithSmallerSize(originalImage, targetImage);
}
private static ImageSize GetTargetResolution(ImageSize imageResolution, Size availableAreaSize, int targetDpi)
{
var scalingFactor = targetDpi / (float)DocumentSettings.DefaultRasterDpi;
var targetResolution = new ImageSize(
(int)(availableAreaSize.Width * scalingFactor),
(int)(availableAreaSize.Height * scalingFactor));
var isImageResolutionSmallerThanTarget = imageResolution.Width < targetResolution.Width || imageResolution.Height < targetResolution.Height;
if (isImageResolutionSmallerThanTarget)
return imageResolution;
return targetResolution;
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false) => IsRendered = false;
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
internal override string? GetCompanionHint()
{
var sizeKB = Math.Max(1, DrawnImageSize / 1024);
return $"{sizeKB}KB";
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Inlined.cs
================================================
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal enum InlinedAlignment
{
Left,
Center,
Right,
Justify,
SpaceAround
}
internal struct InlinedMeasurement
{
public Element Element { get; set; }
public SpacePlan Size { get; set; }
}
internal sealed class Inlined : Element, IContentDirectionAware, IStateful
{
public List Elements { get; internal set; } = new();
public ContentDirection ContentDirection { get; set; }
internal float VerticalSpacing { get; set; }
internal float HorizontalSpacing { get; set; }
internal InlinedAlignment? ElementsAlignment { get; set; }
internal VerticalAlignment BaselineAlignment { get; set; }
internal override IEnumerable GetChildren()
{
return Elements;
}
internal override SpacePlan Measure(Size availableSpace)
{
SetDefaultAlignment();
if (CurrentRenderingIndex == Elements.Count)
return SpacePlan.Empty();
var lines = Compose(availableSpace);
if (!lines.Any())
return SpacePlan.Wrap("The available space is not sufficient to fully render even a single item.");
var lineSizes = lines
.Select(line =>
{
var size = GetLineSize(line);
var widthWithSpacing = size.Width + (line.Count - 1) * HorizontalSpacing;
return new Size(widthWithSpacing, size.Height);
})
.ToList();
var width = lineSizes.Max(x => x.Width);
var height = lineSizes.Sum(x => x.Height) + (lines.Count - 1) * VerticalSpacing;
var targetSize = new Size(width, height);
var totalRenderedItems = CurrentRenderingIndex + lines.Sum(x => x.Count);
var willBeFullyRendered = totalRenderedItems == Elements.Count;
return willBeFullyRendered
? SpacePlan.FullRender(targetSize)
: SpacePlan.PartialRender(targetSize);
}
internal override void Draw(Size availableSpace)
{
// TODO: empty elements should not introduce spacing?
SetDefaultAlignment();
var lines = Compose(availableSpace);
var topOffset = 0f;
foreach (var line in lines)
{
var height = line.Max(x => x.Size.Height);
DrawLine(line);
topOffset += height + VerticalSpacing;
Canvas.Translate(new Position(0, height + VerticalSpacing));
}
Canvas.Translate(new Position(0, -topOffset));
var fullyRenderedItems = lines.Sum(x => x.Count);
CurrentRenderingIndex += fullyRenderedItems;
void DrawLine(ICollection lineMeasurements)
{
var lineSize = GetLineSize(lineMeasurements);
var elementOffset = ElementOffset();
var leftOffset = AlignOffset();
foreach (var measurement in lineMeasurements)
{
var size = (Size)measurement.Size;
var baselineOffset = BaselineOffset(size, lineSize.Height);
if (size.Height == 0)
size = new Size(size.Width, lineSize.Height);
var offset = ContentDirection == ContentDirection.LeftToRight
? new Position(leftOffset, baselineOffset)
: new Position(availableSpace.Width - size.Width - leftOffset, baselineOffset);
Canvas.Translate(offset);
measurement.Element.Draw(size);
Canvas.Translate(offset.Reverse());
leftOffset += size.Width + elementOffset;
}
float ElementOffset()
{
var difference = availableSpace.Width - lineSize.Width;
if (lineMeasurements.Count == 1)
return 0;
return ElementsAlignment switch
{
InlinedAlignment.Justify => difference / (lineMeasurements.Count - 1),
InlinedAlignment.SpaceAround => difference / (lineMeasurements.Count + 1),
_ => HorizontalSpacing
};
}
float AlignOffset()
{
var emptySpace = availableSpace.Width - lineSize.Width - (lineMeasurements.Count - 1) * HorizontalSpacing;
return ElementsAlignment switch
{
InlinedAlignment.Left => ContentDirection == ContentDirection.LeftToRight ? 0 : emptySpace,
InlinedAlignment.Justify => 0,
InlinedAlignment.SpaceAround => elementOffset,
InlinedAlignment.Center => emptySpace / 2,
InlinedAlignment.Right => ContentDirection == ContentDirection.LeftToRight ? emptySpace : 0,
_ => 0
};
}
float BaselineOffset(Size elementSize, float lineHeight)
{
var difference = lineHeight - elementSize.Height;
return BaselineAlignment switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Middle => difference / 2,
_ => difference
};
}
}
}
void SetDefaultAlignment()
{
if (ElementsAlignment.HasValue)
return;
ElementsAlignment = ContentDirection == ContentDirection.LeftToRight
? InlinedAlignment.Left
: InlinedAlignment.Right;
}
static Size GetLineSize(ICollection measurements)
{
var width = measurements.Sum(x => x.Size.Width);
var height = measurements.Max(x => x.Size.Height);
return new Size(width, height);
}
// list of lines, each line is a list of elements
private ICollection> Compose(Size availableSize)
{
var localRenderingIndex = CurrentRenderingIndex;
var result = new List>();
var topOffset = 0f;
while (true)
{
var line = GetNextLine();
if (!line.Any())
break;
var height = line.Max(x => x.Size.Height);
if (topOffset + height > availableSize.Height + Size.Epsilon)
break;
topOffset += height + VerticalSpacing;
result.Add(line);
}
return result;
ICollection GetNextLine()
{
var result = new List();
var leftOffset = GetInitialAlignmentOffset();
while (true)
{
if (localRenderingIndex == Elements.Count)
break;
var element = Elements[localRenderingIndex];
var size = element.Measure(new Size(availableSize.Width, Size.Max.Height));
if (size.Type == SpacePlanType.Wrap)
break;
if (leftOffset + size.Width > availableSize.Width + Size.Epsilon)
break;
localRenderingIndex++;
leftOffset += size.Width + HorizontalSpacing;
result.Add(new InlinedMeasurement
{
Element = element,
Size = size
});
}
return result;
}
float GetInitialAlignmentOffset()
{
// this method makes sure that the spacing between elements is no lesser than configured
return ElementsAlignment switch
{
InlinedAlignment.SpaceAround => HorizontalSpacing * 2,
_ => 0
};
}
}
#region IStateful
private int CurrentRenderingIndex { get; set; }
public void ResetState(bool hardReset = false) => CurrentRenderingIndex = 0;
public object GetState() => CurrentRenderingIndex;
public void SetState(object state) => CurrentRenderingIndex = (int) state;
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/Layers.cs
================================================
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Layer : ContainerElement
{
public bool IsPrimary { get; set; }
}
internal sealed class Layers : Element
{
public List Children { get; set; } = new();
internal override IEnumerable GetChildren()
{
return Children;
}
internal override SpacePlan Measure(Size availableSpace)
{
var measurement = Children
.Single(x => x.IsPrimary)
.Measure(availableSpace);
if (measurement.Type == SpacePlanType.Wrap)
return SpacePlan.Wrap("The content of the primary layer does not fit (even partially) the available space.");
return measurement;
}
internal override void Draw(Size availableSpace)
{
Children
.Where(x => x.Measure(availableSpace).Type != SpacePlanType.Wrap)
.ToList()
.ForEach(x => x.Draw(availableSpace));
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Lazy.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements;
internal sealed class Lazy : ContainerElement, ISemanticAware, IContentDirectionAware, IStateful
{
public SemanticTreeManager? SemanticTreeManager { get; set; }
private SemanticTreeSnapshots? SemanticTreeSnapshots { get; set; }
public Action ContentSource { get; set; }
public bool IsCacheable { get; set; }
internal TextStyle TextStyle { get; set; } = TextStyle.Default;
public ContentDirection ContentDirection { get; set; }
internal int? ImageTargetDpi { get; set; }
internal ImageCompressionQuality? ImageCompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
internal bool ClearCacheAfterFullRender { get; set; } = true;
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
PopulateContent();
return Child.Measure(availableSpace);
}
internal override void Draw(Size availableSpace)
{
if (IsRendered)
return;
SemanticTreeSnapshots ??= new SemanticTreeSnapshots(SemanticTreeManager, PageContext);
using var scope = SemanticTreeSnapshots.StartSemanticStateScope(RenderCount);
PopulateContent();
var isFullyRendered = Child?.Measure(availableSpace).Type == SpacePlanType.FullRender;
Child?.Draw(availableSpace);
RenderCount++;
if (isFullyRendered && ClearCacheAfterFullRender)
{
IsRendered = true;
Child.ReleaseDisposableChildren();
Child = Empty.Instance;
}
}
private void PopulateContent()
{
if (Child is not Empty)
return;
var container = new Container();
Child = container;
ContentSource(container);
if (SemanticTreeManager != null)
{
container.ApplySemanticParagraphs();
container.InjectSemanticTreeManager(SemanticTreeManager);
}
container.ApplyInheritedAndGlobalTexStyle(TextStyle);
container.ApplyContentDirection(ContentDirection);
container.ApplyDefaultImageConfiguration(ImageTargetDpi.Value, ImageCompressionQuality.Value, UseOriginalImage);
container.InjectDependencies(PageContext, Canvas);
container.VisitChildren(x => (x as IStateful)?.ResetState());
}
#region IStateful
public struct LazyState
{
public int RenderCount;
public bool IsRendered;
}
private int RenderCount { get; set; }
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false)
{
if (hardReset)
{
IsRendered = false;
RenderCount = 0;
}
}
public object GetState()
{
return new LazyState
{
RenderCount = RenderCount,
IsRendered = IsRendered
};
}
public void SetState(object state)
{
var lazyState = (LazyState) state;
RenderCount = lazyState.RenderCount;
IsRendered = lazyState.IsRendered;
}
#endregion
}
================================================
FILE: Source/QuestPDF/Elements/Line.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements
{
internal enum LineType
{
Vertical,
Horizontal
}
internal sealed class Line : Element, IStateful
{
public LineType Type { get; set; } = LineType.Vertical;
public Color Color { get; set; } = Colors.Black;
public float Thickness { get; set; } = 1;
public float[] DashPattern { get; set; } = [];
public Color[] GradientColors { get; set; } = [];
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
// TODO: this code is defensive, taking into account conditions below, it is not needed
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
if (Type == LineType.Vertical)
{
if (Thickness.IsGreaterThan(availableSpace.Width))
return SpacePlan.Wrap("The line thickness is greater than the available horizontal space.");
return SpacePlan.FullRender(Thickness, 0);
}
if (Type == LineType.Horizontal)
{
if (Thickness.IsGreaterThan(availableSpace.Height))
return SpacePlan.Wrap("The line thickness is greater than the available vertical space.");
return SpacePlan.FullRender(0, Thickness);
}
// Stryker disable once: unreachable code
throw new NotSupportedException();
}
internal override void Draw(Size availableSpace)
{
if (IsRendered)
return;
var start = Position.Zero;
var end = Type == LineType.Vertical
? new Position(0, availableSpace.Height)
: new Position(availableSpace.Width, 0);
using var paint = new SkPaint();
paint.SetStroke(Thickness);
paint.SetSolidColor(Color);
if (GradientColors.Length > 0)
paint.SetLinearGradient(start, end, GradientColors);
if (DashPattern.Length > 0)
paint.SetDashedPathEffect(DashPattern);
var offset = Type == LineType.Vertical
? new Position(Thickness / 2, 0)
: new Position(0, Thickness / 2);
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(SkSemanticNodeSpecialId.LayoutArtifact);
Canvas.Translate(offset);
Canvas.DrawLine(start, end, paint);
Canvas.Translate(offset.Reverse());
IsRendered = true;
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false) => IsRendered = false;
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
internal override string? GetCompanionHint()
{
return $"{Type} {Thickness.FormatAsCompanionNumber()}";
}
}
}
================================================
FILE: Source/QuestPDF/Elements/MultiColumn.cs
================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements.Text;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements;
internal sealed class MultiColumnChildDrawingObserver : ElementProxy
{
public bool HasBeenDrawn => ChildStateBeforeDrawingOperation != null;
public object? ChildStateBeforeDrawingOperation { get; private set; }
internal override void Draw(Size availableSpace)
{
ChildStateBeforeDrawingOperation ??= (GetFirstElementChild() as IStateful).GetState();
Child.Draw(availableSpace);
}
internal void ResetDrawingState()
{
ChildStateBeforeDrawingOperation = null;
}
internal void RestoreState()
{
(GetFirstElementChild() as IStateful)?.SetState(ChildStateBeforeDrawingOperation);
}
private Element GetFirstElementChild()
{
var child = Child;
while (child is ElementProxy proxy)
child = proxy.Child;
return child;
}
}
internal sealed class MultiColumn : Element, IContentDirectionAware, IDisposable
{
// items
internal Element Content { get; set; } = Empty.Instance;
internal Element Spacer { get; set; } = Empty.Instance;
// configuration
public int ColumnCount { get; set; } = 2;
public bool BalanceHeight { get; set; } = false;
public float Spacing { get; set; }
public ContentDirection ContentDirection { get; set; }
// cache
private ProxyDrawingCanvas ChildrenCanvas { get; } = new();
private TreeNode[] State { get; set; }
~MultiColumn()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
ChildrenCanvas?.Dispose();
GC.SuppressFinalize(this);
}
internal override void CreateProxy(Func create)
{
Content = create(Content);
Spacer = create(Spacer);
}
internal override IEnumerable GetChildren()
{
yield return Content;
yield return Spacer;
}
private void BuildState()
{
if (State != null)
return;
this.VisitChildren(child =>
{
child.CreateProxy(x => x is IStateful ? new MultiColumnChildDrawingObserver { Child = x } : x);
});
State = this.ExtractElementsOfType().ToArray();
}
internal override SpacePlan Measure(Size availableSpace)
{
BuildState();
OptimizeTextCacheBehavior();
if (Content.Canvas != ChildrenCanvas)
Content.InjectDependencies(PageContext, ChildrenCanvas);
ChildrenCanvas.Target = new DiscardDrawingCanvas();
return FindPerfectSpace();
IEnumerable MeasureColumns(Size availableSpace)
{
var columnAvailableSpace = GetAvailableSpaceForColumn(availableSpace);
foreach (var _ in Enumerable.Range(0, ColumnCount))
{
yield return Content.Measure(columnAvailableSpace);
Content.Draw(columnAvailableSpace);
}
ResetObserverState(restoreChildState: true);
}
SpacePlan FindPerfectSpace()
{
var defaultMeasurement = MeasureColumns(availableSpace).ToArray();
if (defaultMeasurement.First().Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return defaultMeasurement.First();
var maxHeight = defaultMeasurement.Max(x => x.Height);
if (defaultMeasurement.Last().Type is SpacePlanType.PartialRender or SpacePlanType.Wrap)
return SpacePlan.PartialRender(availableSpace.Width, maxHeight);
if (!BalanceHeight)
return SpacePlan.FullRender(availableSpace.Width, maxHeight);
var minHeight = 0f;
maxHeight = availableSpace.Height;
foreach (var _ in Enumerable.Range(0, 8))
{
var middleHeight = (minHeight + maxHeight) / 2;
var middleMeasurement = MeasureColumns(new Size(availableSpace.Width, middleHeight));
if (middleMeasurement.Last().Type is SpacePlanType.Empty or SpacePlanType.FullRender)
maxHeight = middleHeight;
else
minHeight = middleHeight;
}
return SpacePlan.FullRender(new Size(availableSpace.Width, maxHeight));
}
}
Size GetAvailableSpaceForColumn(Size totalSpace)
{
var columnWidth = (totalSpace.Width - Spacing * (ColumnCount - 1)) / ColumnCount;
return new Size(columnWidth, totalSpace.Height);
}
internal override void Draw(Size availableSpace)
{
var contentAvailableSpace = GetAvailableSpaceForColumn(availableSpace);
var spacerAvailableSpace = new Size(Spacing, availableSpace.Height);
var horizontalOffset = 0f;
ChildrenCanvas.Target = Canvas;
foreach (var i in Enumerable.Range(1, ColumnCount))
{
var contentMeasurement = Content.Measure(contentAvailableSpace);
var targetColumnSize = new Size(contentAvailableSpace.Width, contentMeasurement.Height);
var contentOffset = GetTargetOffset(targetColumnSize.Width);
Canvas.Translate(contentOffset);
Content.Draw(targetColumnSize);
Canvas.Translate(contentOffset.Reverse());
horizontalOffset += contentAvailableSpace.Width;
if (contentMeasurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
break;
var spacerMeasurement = Spacer.Measure(spacerAvailableSpace);
if (i == ColumnCount || spacerMeasurement.Type is SpacePlanType.Wrap)
continue;
var spacerOffset = GetTargetOffset(Spacing);
Canvas.Translate(spacerOffset);
Spacer.Draw(spacerAvailableSpace);
Canvas.Translate(spacerOffset.Reverse());
horizontalOffset += Spacing;
}
ResetObserverState(restoreChildState: false);
Position GetTargetOffset(float contentWidth)
{
return ContentDirection == ContentDirection.LeftToRight
? new Position(horizontalOffset, 0)
: new Position(availableSpace.Width - horizontalOffset - contentWidth, 0);
}
}
void ResetObserverState(bool restoreChildState)
{
foreach (var node in State)
Traverse(node);
void Traverse(TreeNode node)
{
var observer = node.Value;
if (!observer.HasBeenDrawn)
return;
if (restoreChildState)
observer.RestoreState();
observer.ResetDrawingState();
foreach (var child in node.Children)
Traverse(child);
}
}
#region Text Optimization
private bool IsTextOptimizationExecuted { get; set; } = false;
///
/// The TextBlock element uses SkParagraph cache to enhance rendering speed.
/// This cache uses a significant amount of memory and is cleared after FullRender.
/// However, the MultiColumn element uses a sophisticated measuring algorithm,
/// and may force the Text element to measure/render multiple times per page.
/// To avoid performance issues, the TextBlock element should keep its cache.
///
private void OptimizeTextCacheBehavior()
{
if (IsTextOptimizationExecuted)
return;
IsTextOptimizationExecuted = true;
Content.VisitChildren(x =>
{
if (x is TextBlock text)
text.ClearInternalCacheAfterFullRender = false;
});
}
#endregion
}
================================================
FILE: Source/QuestPDF/Elements/Padding.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Padding : ContainerElement
{
public float Top { get; set; }
public float Right { get; set; }
public float Bottom { get; set; }
public float Left { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
var internalSpace = InternalSpace(availableSpace);
if (internalSpace.IsNegative())
return Child.IsEmpty() ? SpacePlan.Empty() : SpacePlan.Wrap("The available space is negative.");
var measure = base.Measure(internalSpace);
if (measure.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return measure;
var newWidth = Math.Max(0, measure.Width + Left + Right);
var newHeight = Math.Max(0, measure.Height + Top + Bottom);
var newSize = new Size(
newWidth,
newHeight);
if (measure.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(newSize);
return SpacePlan.FullRender(newSize);
}
internal override void Draw(Size availableSpace)
{
var internalSpace = InternalSpace(availableSpace);
Canvas.Translate(new Position(Left, Top));
base.Draw(internalSpace);
Canvas.Translate(new Position(-Left, -Top));
}
private Size InternalSpace(Size availableSpace)
{
return new Size(
availableSpace.Width - Left - Right,
availableSpace.Height - Top - Bottom);
}
internal override string? GetCompanionHint()
{
return string.Join(" ", GetOptions().Where(x => x.value != 0).Select(x => $"{x.Label}={x.value.FormatAsCompanionNumber()}"));
IEnumerable<(string Label, float value)> GetOptions()
{
if (Top == Bottom && Right == Left && Top == Right)
{
yield return ("A", Top);
yield break;
}
if (Top == Bottom && Right == Left)
{
yield return ("V", Top);
yield return ("H", Left);
yield break;
}
yield return ("L", Left);
yield return ("T", Top);
yield return ("R", Right);
yield return ("B", Bottom);
}
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Page.cs
================================================
using System;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements
{
internal sealed class Page : IComponent
{
public ContentDirection ContentDirection { get; set; }
public TextStyle DefaultTextStyle { get; set; } = TextStyle.Default;
public Size? MinSize { get; set; }
public Size? MaxSize { get; set; }
public float MarginLeft { get; set; }
public float MarginRight { get; set; }
public float MarginTop { get; set; }
public float MarginBottom { get; set; }
public Color BackgroundColor { get; set; } = Colors.Transparent;
public Element Background { get; set; } = Empty.Instance;
public Element Foreground { get; set; } = Empty.Instance;
public Element Header { get; set; } = Empty.Instance;
public Element Content { get; set; } = Empty.Instance;
public Element Footer { get; set; } = Empty.Instance;
public void Compose(IContainer container)
{
SetDefaultPageSizeIfNotSpecified();
container
.DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Page.ToString())
.ContentDirection(ContentDirection)
.DefaultTextStyle(DefaultTextStyle.DisableFontFeature(FontFeatures.StandardLigatures))
.Layers(layers =>
{
layers.Layer()
.ZIndex(int.MinValue)
.Background(BackgroundColor);
layers
.Layer()
.Repeat()
.DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Background.ToString())
.Artifact(SkSemanticNodeSpecialId.BackgroundArtifact)
.Element(Background);
layers
.PrimaryLayer()
.MinWidth(MinSize?.Width ?? 0)
.MinHeight(MinSize?.Height ?? 0)
.MaxWidth(MaxSize?.Width ?? Size.Max.Width)
.MaxHeight(MaxSize?.Height ?? Size.Max.Height)
.EnforceSizeWhenEmpty()
.PaddingLeft(MarginLeft)
.PaddingRight(MarginRight)
.PaddingTop(MarginTop)
.PaddingBottom(MarginBottom)
.Decoration(decoration =>
{
decoration
.Before()
.DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Header.ToString())
.Element(Header);
decoration
.Content()
.NonTrackingElement(x => (MinSize?.Width == MaxSize?.Width) ? x.ExtendHorizontal() : x)
.NonTrackingElement(x => (MinSize?.Height == MaxSize?.Height) ? x.ExtendVertical() : x)
.DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Content.ToString())
.Element(Content);
decoration
.After()
.DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Footer.ToString())
.Element(Footer);
});
layers
.Layer()
.Repeat()
.Artifact(SkSemanticNodeSpecialId.PaginationWatermarkArtifact)
.DebugPointer(DebugPointerType.DocumentStructure, DocumentStructureTypes.Foreground.ToString())
.Element(Foreground);
});
}
private void SetDefaultPageSizeIfNotSpecified()
{
if (MinSize.HasValue || MaxSize.HasValue)
return;
MinSize = PageSizes.A4;
MaxSize = PageSizes.A4;
}
}
}
================================================
FILE: Source/QuestPDF/Elements/PageBreak.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class PageBreak : Element, IStateful
{
internal override SpacePlan Measure(Size availableSpace)
{
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
if (IsRendered)
return SpacePlan.Empty();
return SpacePlan.PartialRender(Size.Zero);
}
internal override void Draw(Size availableSpace)
{
IsRendered = true;
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false) => IsRendered = false;
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/Placeholder.cs
================================================
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Placeholder : IComponent
{
public string Text { get; set; }
public void Compose(IContainer container)
{
const float imageSvgSize = 24f;
const string imageSvgPath = "M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13.96,12.29L11.21,15.83L9.25,13.47L6.5,17H17.5L13.96,12.29Z";
container
.Background(Colors.Grey.Lighten2)
.Padding(5)
.AlignMiddle()
.AlignCenter()
.Element(x =>
{
if (string.IsNullOrWhiteSpace(Text))
x.Height(imageSvgSize).Width(imageSvgSize).SvgPath(imageSvgPath, Colors.Black);
else
x.Text(Text).FontSize(14);
});
}
}
}
================================================
FILE: Source/QuestPDF/Elements/PreventPageBreak.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class PreventPageBreak : ContainerElement, IStateful
{
internal override SpacePlan Measure(Size availableSpace)
{
var measurement = base.Measure(availableSpace);
if (IsFirstPageRendered)
return measurement;
if (measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
return measurement;
return SpacePlan.PartialRender(Size.Zero);
}
internal override void Draw(Size availableSpace)
{
if (IsFirstPageRendered)
{
base.Draw(availableSpace);
return;
}
var measurement = base.Measure(availableSpace);
if (measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
base.Draw(availableSpace);
IsFirstPageRendered = true;
}
#region IStateful
private bool IsFirstPageRendered { get; set; }
public void ResetState(bool hardReset = false)
{
if (hardReset)
IsFirstPageRendered = false;
}
public object GetState() => IsFirstPageRendered;
public void SetState(object state) => IsFirstPageRendered = (bool) state;
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/RepeatContent.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Elements.Text;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements;
internal sealed class RepeatContent : ContainerElement, IStateful, ISemanticAware
{
public SemanticTreeManager? SemanticTreeManager { get; set; }
public enum RepeatContextType
{
PageHeader,
PageFooter,
Other
}
public RepeatContextType RepeatContext { get; set; } = RepeatContextType.Other;
internal override void Draw(Size availableSpace)
{
OptimizeContentCacheBehavior();
var childMeasurement = Child.Measure(availableSpace);
if (SemanticTreeManager == null)
{
base.Draw(availableSpace);
ResetChildrenIfNecessary();
return;
}
if (IsFullyRendered)
{
var paginationNodeId = RepeatContext switch
{
RepeatContextType.PageHeader => SkSemanticNodeSpecialId.PaginationHeaderArtifact,
RepeatContextType.PageFooter => SkSemanticNodeSpecialId.PaginationFooterArtifact,
_ => SkSemanticNodeSpecialId.PaginationArtifact
};
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(paginationNodeId);
SemanticTreeManager.BeginArtifactContent();
base.Draw(availableSpace);
SemanticTreeManager.EndArtifactContent();
}
else
{
base.Draw(availableSpace);
}
ResetChildrenIfNecessary();
void ResetChildrenIfNecessary()
{
if (childMeasurement.Type != SpacePlanType.FullRender)
return;
Child.VisitChildren(x => (x as IStateful)?.ResetState(false));
IsFullyRendered = true;
}
}
#region Text Optimization
private bool IsContentOptimizationExecuted { get; set; } = false;
///
///
/// The TextBlock element uses SkParagraph cache to enhance rendering speed.
/// This cache uses a significant amount of memory and is cleared after FullRender.
/// However, when using the RepeatContent element, the cache is cleared after each repetition.
/// To avoid performance issues, the default behavior is disabled.
///
///
///
/// Similarly, the Lazy element builds entire content on demand, waits to fully render it and then removes it.
/// This aims to optimize managed memory usage.
/// However, it may not be the most optimal solution in repeating context.
///
///
private void OptimizeContentCacheBehavior()
{
if (IsContentOptimizationExecuted)
return;
IsContentOptimizationExecuted = true;
Child.VisitChildren(x =>
{
if (x is TextBlock text)
text.ClearInternalCacheAfterFullRender = false;
if (x is Lazy lazy)
lazy.ClearCacheAfterFullRender = false;
});
}
#endregion
#region IStateful
private bool IsFullyRendered { get; set; }
public void ResetState(bool hardReset = false)
{
if (hardReset)
IsFullyRendered = false;
}
public object GetState() => IsFullyRendered;
public void SetState(object state) => IsFullyRendered = (bool) state;
#endregion
}
================================================
FILE: Source/QuestPDF/Elements/Rotate.cs
================================================
using System;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Rotate : ContainerElement
{
public float Angle { get; set; } = 0;
internal override void Draw(Size availableSpace)
{
Canvas.Rotate(Angle);
Child?.Draw(availableSpace);
Canvas.Rotate(-Angle);
}
internal override string? GetCompanionHint()
{
if (Angle == 0)
return "No rotation";
var degrees = Math.Abs(Angle);
// Stryker disable once equality: TurnCount = 0 is handled above
var direction = Angle > 0 ? "clockwise" : "counter-clockwise";
return $"{degrees.FormatAsCompanionNumber()} deg {direction}";
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Row.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal enum RowItemType
{
Auto,
Constant,
Relative
}
internal sealed class RowItem : ContainerElement
{
public bool IsRendered { get; set; }
public float Width { get; set; }
public RowItemType Type { get; set; }
public float Size { get; set; }
internal override string? GetCompanionHint()
{
if (Type == RowItemType.Auto)
return "Auto";
return $"{Type} {Size.FormatAsCompanionNumber()}";
}
}
internal sealed class RowItemRenderingCommand
{
public RowItem RowItem { get; set; }
public SpacePlan Measurement { get; set; }
public Size Size { get; set; }
public Position Offset { get; set; }
}
internal sealed class Row : Element, IStateful, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
internal List Items { get; } = new();
internal float Spacing { get; set; }
internal override IEnumerable GetChildren()
{
return Items;
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!Items.Any())
return SpacePlan.Empty();
if (Items.All(x => x.IsRendered))
return SpacePlan.Empty();
UpdateItemsWidth(availableSpace.Width);
if (Items.Any(x => x.Width.IsLessThan(0)))
return SpacePlan.Wrap("One of the items has a negative size, indicating insufficient horizontal space. Usually, constant items require more space than is available, potentially causing other content to overflow.");
var renderingCommands = PlanLayout(availableSpace);
if (renderingCommands.Any(x => !x.RowItem.IsRendered && x.Measurement.Type == SpacePlanType.Wrap))
return SpacePlan.Wrap("One of the items does not fit (even partially) in the available space.");
var width = renderingCommands.Last().Offset.X + renderingCommands.Last().Size.Width;
var height = renderingCommands.Max(x => x.Size.Height);
var size = new Size(width, height);
if (width.IsGreaterThan(availableSpace.Width))
return SpacePlan.Wrap("The content requires more horizontal space than available.");
if (height.IsGreaterThan(availableSpace.Height))
return SpacePlan.Wrap("The content requires more vertical space than available.");
if (renderingCommands.Any(x => !x.RowItem.IsRendered && x.Measurement.Type == SpacePlanType.PartialRender))
return SpacePlan.PartialRender(size);
return SpacePlan.FullRender(size);
}
internal override void Draw(Size availableSpace)
{
if (!Items.Any())
return;
if (Items.All(x => x.IsRendered))
return;
UpdateItemsWidth(availableSpace.Width);
var renderingCommands = PlanLayout(availableSpace);
foreach (var command in renderingCommands)
{
if (command.Measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
command.RowItem.IsRendered = true;
// TODO: investigate, as the final targetSize is still changed to use available vertical space
if (command.Measurement.Type is SpacePlanType.Wrap)
continue;
var offset = ContentDirection == ContentDirection.LeftToRight
? command.Offset
: new Position(availableSpace.Width - command.Offset.X - command.Size.Width, 0);
var targetSize = new Size(command.Size.Width, availableSpace.Height);
if (targetSize.Width.IsLessThan(0))
continue;
Canvas.Translate(offset);
command.RowItem.Draw(targetSize);
Canvas.Translate(offset.Reverse());
}
}
private void UpdateItemsWidth(float availableWidth)
{
foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto && x.Size == 0))
rowItem.Size = rowItem.Measure(Size.Max).Width;
var constantWidth = Items.Where(x => x.Type != RowItemType.Relative).Sum(x => x.Size);
var relativeWidth = Items.Where(x => x.Type == RowItemType.Relative).Sum(x => x.Size);
var spacingWidth = (Items.Count - 1) * Spacing;
foreach (var item in Items.Where(x => x.Type != RowItemType.Relative))
item.Width = item.Size;
if (relativeWidth <= 0)
return;
var widthPerRelativeUnit = (availableWidth - constantWidth - spacingWidth) / relativeWidth;
foreach (var item in Items.Where(x => x.Type == RowItemType.Relative))
item.Width = item.Size * widthPerRelativeUnit;
}
private ICollection PlanLayout(Size availableSpace)
{
var leftOffset = 0f;
var renderingCommands = new List();
foreach (var item in Items)
{
var itemSpace = new Size(item.Width, availableSpace.Height);
var command = new RowItemRenderingCommand
{
RowItem = item,
Size = itemSpace,
Measurement = item.Measure(itemSpace),
Offset = new Position(leftOffset, 0)
};
renderingCommands.Add(command);
leftOffset += item.Width + Spacing;
}
// TODO: investigate
if (renderingCommands.Any(x => x.Measurement.Type == SpacePlanType.Wrap))
return renderingCommands;
var rowHeight = renderingCommands
.Where(x => !x.RowItem.IsRendered)
.Select(x => x.Measurement.Height)
.DefaultIfEmpty(0)
.Max();
foreach (var command in renderingCommands)
{
command.Size = new Size(command.Size.Width, rowHeight);
command.Measurement = command.RowItem.Measure(command.Size);
}
return renderingCommands;
}
#region IStateful
// State is stored in the RowItem instances
public void ResetState(bool hardReset = false)
{
foreach (var rowItem in Items)
{
rowItem.IsRendered = false;
// required when the row contains items with text representing page numbers
if (rowItem.Type == RowItemType.Auto)
rowItem.Size = 0;
}
}
public object GetState()
{
var result = new bool[Items.Count];
for (var i = 0; i < Items.Count; i++)
result[i] = Items[i].IsRendered;
return result;
}
public void SetState(object state)
{
var states = (bool[]) state;
for (var i = 0; i < Items.Count; i++)
Items[i].IsRendered = states[i];
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/Scale.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Scale : ContainerElement
{
public float ScaleX { get; set; } = 1;
public float ScaleY { get; set; } = 1;
internal override SpacePlan Measure(Size availableSpace)
{
var targetSpace = new Size(
Math.Abs(availableSpace.Width / ScaleX),
Math.Abs(availableSpace.Height / ScaleY));
var measure = base.Measure(targetSpace);
if (measure.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return measure;
var targetSize = new Size(
Math.Abs(measure.Width * ScaleX),
Math.Abs(measure.Height * ScaleY));
if (measure.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(targetSize);
if (measure.Type == SpacePlanType.FullRender)
return SpacePlan.FullRender(targetSize);
// Stryker disable once: unreachable code
throw new ArgumentException();
}
internal override void Draw(Size availableSpace)
{
var targetSpace = new Size(
Math.Abs(availableSpace.Width / ScaleX),
Math.Abs(availableSpace.Height / ScaleY));
var translate = new Position(
ScaleX < 0 ? availableSpace.Width : 0,
ScaleY < 0 ? availableSpace.Height : 0);
Canvas.Translate(translate);
Canvas.Scale(ScaleX, ScaleY);
Child?.Draw(targetSpace);
Canvas.Scale(1/ScaleX, 1/ScaleY);
Canvas.Translate(translate.Reverse());
}
internal override string? GetCompanionHint()
{
return string.Join(" ", GetOptions().Where(x => x.value != 1).Select(x => $"{x.Label}={x.value.FormatAsCompanionNumber()}"));
IEnumerable<(string Label, float value)> GetOptions()
{
if (ScaleX == ScaleY)
{
yield return ("A", ScaleX);
yield break;
}
yield return ("H", ScaleX);
yield return ("V", ScaleY);
}
}
}
}
================================================
FILE: Source/QuestPDF/Elements/ScaleToFit.cs
================================================
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class ScaleToFit : ContainerElement
{
internal override SpacePlan Measure(Size availableSpace)
{
var perfectScale = FindPerfectScale(availableSpace);
if (perfectScale == null)
return SpacePlan.Wrap("Cannot find the perfect scale to fit the child element in the available space.");
var scaledSpace = ScaleSize(availableSpace, 1 / perfectScale.Value);
var childSizeInScale = base.Measure(scaledSpace);
var childSizeInOriginalScale = ScaleSize(childSizeInScale, perfectScale.Value);
return SpacePlan.FullRender(childSizeInOriginalScale);
}
internal override void Draw(Size availableSpace)
{
var perfectScale = FindPerfectScale(availableSpace);
if (!perfectScale.HasValue)
return;
var targetScale = perfectScale.Value;
var targetSpace = ScaleSize(availableSpace, 1 / targetScale);
Canvas.Scale(targetScale, targetScale);
Child?.Draw(targetSpace);
Canvas.Scale(1 / targetScale, 1 / targetScale);
}
private static Size ScaleSize(Size size, float factor)
{
return new Size(size.Width * factor, size.Height * factor);
}
private float? FindPerfectScale(Size availableSpace)
{
if (ChildFits(1))
return 1;
var maxScale = 1f;
var minScale = Size.Epsilon;
var lastWorkingScale = (float?)null;
foreach (var _ in Enumerable.Range(0, 8))
{
var halfScale = (maxScale + minScale) / 2;
if (ChildFits(halfScale))
{
minScale = halfScale;
lastWorkingScale = halfScale;
}
else
{
maxScale = halfScale;
}
}
return lastWorkingScale;
bool ChildFits(float scale)
{
var scaledSpace = ScaleSize(availableSpace, 1 / scale);
return base.Measure(scaledSpace).Type is SpacePlanType.Empty or SpacePlanType.FullRender;
}
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Section.cs
================================================
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Section : ContainerElement, IStateful
{
public string SectionName { get; set; }
internal override void Draw(Size availableSpace)
{
if (!IsRendered)
{
var targetName = PageContext.GetDocumentLocationName(SectionName);
Canvas.DrawSection(targetName);
IsRendered = true;
}
PageContext.SetSectionPage(SectionName);
base.Draw(availableSpace);
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false) => IsRendered = false;
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
internal override string? GetCompanionHint() => SectionName;
internal override string? GetCompanionSearchableContent() => SectionName;
internal override IEnumerable>? GetCompanionProperties()
{
yield return new("SectionName", SectionName);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/SectionLink.cs
================================================
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class SectionLink : ContainerElement
{
public string SectionName { get; set; }
public string? Description { get; set; }
internal override void Draw(Size availableSpace)
{
var targetSize = base.Measure(availableSpace);
if (targetSize.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return;
var targetName = PageContext.GetDocumentLocationName(SectionName);
Canvas.DrawSectionLink(targetSize, targetName, Description);
base.Draw(availableSpace);
}
internal override string? GetCompanionHint() => SectionName;
internal override string? GetCompanionSearchableContent() => SectionName;
internal override IEnumerable>? GetCompanionProperties()
{
yield return new("SectionName", SectionName);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/SemanticTag.cs
================================================
using System;
using System.Text;
using QuestPDF.Drawing;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Elements.Text;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements;
internal class SemanticTag : ContainerElement, ISemanticAware
{
public SemanticTreeManager? SemanticTreeManager { get; set; }
public SemanticTreeNode? SemanticTreeNode { get; private set; }
public string TagType { get; set; }
public string? Alt { get; set; }
public string? Lang { get; set; }
internal override void Draw(Size availableSpace)
{
var shouldIgnoreSemanticMeaning =
Canvas.Is() ||
SemanticTreeManager == null ||
SemanticTreeManager.IsCurrentContentArtifact();
if (shouldIgnoreSemanticMeaning)
{
Child?.Draw(availableSpace);
return;
}
RegisterCurrentSemanticNode();
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(SemanticTreeNode.NodeId);
SemanticTreeManager.PushOnStack(SemanticTreeNode);
Child?.Draw(availableSpace);
SemanticTreeManager.PopStack();
}
internal void RegisterCurrentSemanticNode()
{
if (SemanticTreeNode != null)
return;
if (TagType is "H" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6")
UpdateHeaderText();
if (TagType is "Link")
UpdateDescriptionOfInnerLink();
var id = SemanticTreeManager.GetNextNodeId();
SemanticTreeNode = new SemanticTreeNode
{
NodeId = id,
Type = TagType,
Alt = Alt,
Lang = Lang
};
SemanticTreeManager.AddNode(SemanticTreeNode);
}
private void UpdateHeaderText()
{
if (!string.IsNullOrWhiteSpace(Alt))
return;
var builder = new StringBuilder();
Traverse(builder, Child);
Alt = builder.ToString();
static void Traverse(StringBuilder builder, Element element)
{
if (element is TextBlock textBlock)
{
if (builder.Length > 0)
builder.Append(' ');
builder.Append(textBlock.Text);
}
else if (element is ContainerElement container)
{
Traverse(builder, container.Child);
}
else
{
foreach (var child in element.GetChildren())
Traverse(builder, child);
}
}
}
private void UpdateDescriptionOfInnerLink()
{
if (string.IsNullOrWhiteSpace(Alt))
return;
var currentChild = Child;
while (currentChild != null)
{
if (currentChild is Hyperlink hyperlink)
{
hyperlink.Description = Alt;
return;
}
if (currentChild is SectionLink sectionLink)
{
sectionLink.Description = Alt;
return;
}
currentChild = (currentChild as ContainerElement)?.Child;
}
}
internal override string? GetCompanionHint()
{
var result = TagType;
if (!string.IsNullOrWhiteSpace(Alt))
result += $" ({Alt})";
return result;
}
internal override string? GetCompanionSearchableContent() => TagType;
}
================================================
FILE: Source/QuestPDF/Elements/ShowEntire.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class ShowEntire : ContainerElement
{
internal override SpacePlan Measure(Size availableSpace)
{
var childMeasurement = base.Measure(availableSpace);
if (childMeasurement.Type is SpacePlanType.Wrap)
return SpacePlan.Wrap("Child element does not fit (even partially) on the page.");
if (childMeasurement.Type is SpacePlanType.PartialRender)
return SpacePlan.Wrap("Child element fits only partially on the page.");
return childMeasurement;
}
}
}
================================================
FILE: Source/QuestPDF/Elements/ShowIf.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements;
public sealed class ShowIfContext
{
public int PageNumber { get; internal set; }
///
/// Returns the total count of pages in the document.
///
///
///
/// Document rendering process is performed in two phases.
/// During the first phase, the value of this property is equal to null to indicate its unavailability.
///
/// Please note that using this property may result with unstable layouts and unpredicted behaviors, especially when generating conditional content of various sizes.
///
public int? TotalPages { get; internal set; }
}
internal sealed class ShowIf : ContainerElement
{
public Predicate VisibilityPredicate { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
if (!CheckVisibility())
return SpacePlan.Empty();
return base.Measure(availableSpace);
}
internal override void Draw(Size availableSpace)
{
if (CheckVisibility())
base.Draw(availableSpace);
}
private bool CheckVisibility()
{
var context = new ShowIfContext
{
PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.IsInitialRenderingPhase ? null : PageContext.DocumentLength
};
return VisibilityPredicate(context);
}
}
================================================
FILE: Source/QuestPDF/Elements/ShowOnce.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class ShowOnce : ContainerElement, IStateful
{
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
return base.Measure(availableSpace);
}
internal override void Draw(Size availableSpace)
{
if (IsRendered)
return;
if (base.Measure(availableSpace).Type is SpacePlanType.Empty or SpacePlanType.FullRender)
IsRendered = true;
base.Draw(availableSpace);
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false)
{
if (hardReset)
IsRendered = false;
}
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/Shrink.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Shrink : ContainerElement, IContentDirectionAware
{
public bool Vertical { get; set; }
public bool Horizontal { get; set; }
public ContentDirection ContentDirection { get; set; }
internal override void Draw(Size availableSpace)
{
var childSize = base.Measure(availableSpace);
if (childSize.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return;
var targetSize = new Size(
Horizontal ? childSize.Width : availableSpace.Width,
Vertical ? childSize.Height : availableSpace.Height);
var translate = ContentDirection == ContentDirection.RightToLeft
? new Position(availableSpace.Width - targetSize.Width, 0)
: Position.Zero;
Canvas.Translate(translate);
base.Draw(targetSize);
Canvas.Translate(translate.Reverse());
}
internal override string? GetCompanionHint()
{
return (Vertical, Horizontal) switch
{
(true, true) => "Both axes",
(true, false) => "Vertical axis",
(false, true) => "Horizontal axis",
(false, false) => null,
};
}
}
}
================================================
FILE: Source/QuestPDF/Elements/SimpleRotate.cs
================================================
using System;
using System.Text;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class SimpleRotate : ContainerElement
{
public int TurnCount { get; set; }
public int NormalizedTurnCount => (TurnCount % 4 + 4) % 4;
internal override SpacePlan Measure(Size availableSpace)
{
if (NormalizedTurnCount == 0 || NormalizedTurnCount == 2)
return base.Measure(availableSpace);
availableSpace = new Size(availableSpace.Height, availableSpace.Width);
var childSpace = base.Measure(availableSpace);
if (childSpace.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return childSpace;
var targetSpace = new Size(childSpace.Height, childSpace.Width);
if (childSpace.Type == SpacePlanType.FullRender)
return SpacePlan.FullRender(targetSpace);
if (childSpace.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(targetSpace);
// Stryker disable once: unreachable code
throw new ArgumentException();
}
internal override void Draw(Size availableSpace)
{
var translate = new Position(
(NormalizedTurnCount == 1 || NormalizedTurnCount == 2) ? availableSpace.Width : 0,
(NormalizedTurnCount == 2 || NormalizedTurnCount == 3) ? availableSpace.Height : 0);
var rotate = NormalizedTurnCount * 90;
Canvas.Translate(translate);
Canvas.Rotate(rotate);
if (NormalizedTurnCount == 1 || NormalizedTurnCount == 3)
availableSpace = new Size(availableSpace.Height, availableSpace.Width);
Child?.Draw(availableSpace);
Canvas.Rotate(-rotate);
Canvas.Translate(translate.Reverse());
}
internal override string? GetCompanionHint()
{
if (TurnCount == 0)
return "No rotation";
var degrees = Math.Abs(TurnCount) * 90;
// Stryker disable once equality: TurnCount = 0 is handled above
var direction = TurnCount > 0 ? "clockwise" : "counter-clockwise";
return $"{degrees} deg {direction}";
}
}
}
================================================
FILE: Source/QuestPDF/Elements/SkipOnce.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class SkipOnce : ContainerElement, IStateful
{
internal override SpacePlan Measure(Size availableSpace)
{
if (!FirstPageWasSkipped)
return SpacePlan.Empty();
return base.Measure(availableSpace);
}
internal override void Draw(Size availableSpace)
{
if (FirstPageWasSkipped)
Child.Draw(availableSpace);
FirstPageWasSkipped = true;
}
#region IStateful
private bool FirstPageWasSkipped { get; set; }
public void ResetState(bool hardReset = false)
{
if (hardReset)
FirstPageWasSkipped = false;
}
public object GetState() => FirstPageWasSkipped;
public void SetState(object state) => FirstPageWasSkipped = (bool) state;
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/SourceCodePointer.cs
================================================
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements;
internal sealed class SourceCodePointer : ContainerElement
{
public string MethodName { get; set; }
public string CalledFrom { get; set; }
public string FilePath { get; set; }
public int LineNumber { get; set; }
internal override string? GetCompanionSearchableContent() => $"{MethodName} {CalledFrom} {FilePath}";
internal override IEnumerable>? GetCompanionProperties()
{
yield return new("MethodName", MethodName);
yield return new("CalledFrom", CalledFrom);
yield return new("FilePath", FilePath);
yield return new("LineNumber", LineNumber.ToString());
}
}
================================================
FILE: Source/QuestPDF/Elements/StopPaging.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class StopPaging : ContainerElement
{
internal override SpacePlan Measure(Size availableSpace)
{
var measurement = base.Measure(availableSpace);
return measurement.Type switch
{
SpacePlanType.Wrap => SpacePlan.FullRender(Size.Zero),
SpacePlanType.PartialRender => SpacePlan.FullRender(measurement),
SpacePlanType.FullRender => measurement,
SpacePlanType.Empty => measurement,
_ => throw new ArgumentOutOfRangeException()
};
}
internal override void Draw(Size availableSpace)
{
var measurement = base.Measure(availableSpace);
if (measurement.Type is SpacePlanType.Wrap)
return;
base.Draw(availableSpace);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/StyledBox.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Drawing.DrawingCanvases;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements
{
internal sealed class StyledBox : ContainerElement
{
public float BorderLeft { get; set; }
public float BorderTop { get; set; }
public float BorderRight { get; set; }
public float BorderBottom { get; set; }
private bool HasBorder =>
BorderLeft > 0 ||
BorderTop > 0 ||
BorderRight > 0 ||
BorderBottom > 0;
private bool HasFullBorder =>
BorderLeft > 0 &&
BorderTop > 0 &&
BorderRight > 0 &&
BorderBottom > 0;
private bool HasUniformBorder =>
BorderLeft == BorderRight &&
BorderTop == BorderBottom &&
BorderLeft == BorderTop;
public float BorderRadiusTopLeft { get; set; }
public float BorderRadiusTopRight { get; set; }
public float BorderRadiusBottomLeft { get; set; }
public float BorderRadiusBottomRight { get; set; }
private bool HasRoundedCorners =>
BorderRadiusTopLeft > 0 ||
BorderRadiusTopRight > 0 ||
BorderRadiusBottomLeft > 0 ||
BorderRadiusBottomRight > 0;
private bool HasUniformRoundedCorners =>
BorderRadiusTopLeft == BorderRadiusTopRight &&
BorderRadiusBottomLeft == BorderRadiusBottomRight &&
BorderRadiusTopLeft == BorderRadiusBottomLeft;
public float? BorderAlignment { get; set; } // 0 = inset, 1 = outset
public Color BackgroundColor { get; set; } = Colors.Transparent;
public Color[] BackgroundGradientColors { get; set; } = [];
public float? BackgroundGradientAngle { get; set; }
public Color BorderColor { get; set; } = Colors.Transparent;
public Color[] BorderGradientColors { get; set; } = [];
public float? BorderGradientAngle { get; set; }
private bool HasSimpleStyle => BackgroundGradientColors.Length == 0 && BorderGradientColors.Length == 0;
public BoxShadowStyle? Shadow { get; set; }
internal void AdjustBorderAlignment()
{
if (BorderAlignment != null)
return;
var shouldHaveInsetBorder = HasRoundedCorners;
BorderAlignment = shouldHaveInsetBorder ? 0f : 0.5f;
}
internal override void Draw(Size availableSpace)
{
AdjustBorderAlignment();
// optimization: do not perform expensive calls
if (Canvas is DiscardDrawingCanvas)
{
base.Draw(availableSpace);
return;
}
using var backgroundPaint = GetBackgroundPaint(availableSpace);
using var borderPaint = GetBorderPaint(availableSpace);
if (HasFullBorder && HasUniformBorder && !HasRoundedCorners && HasSimpleStyle && BorderAlignment == 0.5f && Shadow == null)
{
// optimization: draw a simple rectangle with border
if (backgroundPaint != null)
{
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(SkSemanticNodeSpecialId.BackgroundArtifact);
Canvas.DrawRectangle(Position.Zero, availableSpace, backgroundPaint);
}
base.Draw(availableSpace);
if (borderPaint != null)
{
borderPaint.SetStroke(BorderLeft);
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(SkSemanticNodeSpecialId.LayoutArtifact);
Canvas.DrawRectangle(Position.Zero, availableSpace, borderPaint);
}
return;
}
var contentRect = GetPrimaryBorderRect(availableSpace);
var borderOuterRect = GetOuterRect(availableSpace);
var borderInnerRect = GetInnerRect(availableSpace);
if (Shadow != null)
{
var shadowRect = ExpandRoundedRect(contentRect, Shadow.Spread);
var canvasShadow = new SkBoxShadow
{
OffsetX = Shadow.OffsetX,
OffsetY = Shadow.OffsetY,
Blur = Shadow.Blur,
Color = Shadow.Color
};
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(SkSemanticNodeSpecialId.BackgroundArtifact);
Canvas.DrawShadow(shadowRect, canvasShadow);
}
if (HasRoundedCorners)
{
Canvas.Save();
Canvas.ClipRoundedRectangle(contentRect);
}
if (backgroundPaint != null)
{
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(SkSemanticNodeSpecialId.BackgroundArtifact);
Canvas.DrawRectangle(Position.Zero, availableSpace, backgroundPaint);
}
base.Draw(availableSpace);
if (HasRoundedCorners)
Canvas.Restore();
if (borderPaint != null)
{
using var semanticScope = Canvas.StartSemanticScopeWithNodeId(SkSemanticNodeSpecialId.LayoutArtifact);
Canvas.DrawComplexBorder(borderInnerRect, borderOuterRect, borderPaint);
}
}
private (Position start, Position end) GetLinearGradientPositions(Size availableSpace, float angle)
{
if (angle == 0f)
return (Position.Zero, new Position(availableSpace.Width, 0));
if (angle == 90f)
return (Position.Zero, new Position(0, availableSpace.Height));
if (angle == 180f)
return (new Position(availableSpace.Width, 0), new Position(0, 0));
if (angle == 270f)
return (new Position(0, availableSpace.Height), new Position(availableSpace.Width, availableSpace.Height));
// other angles?
var rectanglePoints = new[]
{
Position.Zero,
new Position(availableSpace.Width, 0),
new Position(availableSpace.Width, availableSpace.Height),
new Position(0, availableSpace.Height)
};
var angleInRadians = Math.PI * angle / 180f;
var linePoint = new Position(availableSpace.Width / 2f, availableSpace.Height / 2f);
var projectedPoints = rectanglePoints
.Select(point => ProjectPointOntoLine(linePoint, (float)angleInRadians, point))
.ToArray();
var start = projectedPoints.OrderBy(p => p.X).First();
var end = projectedPoints.OrderByDescending(p => p.X).First();
return (start, end);
static Position ProjectPointOntoLine(Position linePoint, float lineAngleRadians, Position projectionPoint)
{
var dx = (float)Math.Cos(lineAngleRadians);
var dy = (float)Math.Sin(lineAngleRadians);
var vx = projectionPoint.X - linePoint.X;
var vy = projectionPoint.Y - linePoint.Y;
var t = vx * dx + vy * dy;
return new Position(
linePoint.X + dx * t,
linePoint.Y + dy * t
);
}
}
private SkPaint? GetBorderPaint(Size availableSpace)
{
if (BorderGradientColors.Length > 0)
{
var paint = new SkPaint();
var gradientPoints = GetLinearGradientPositions(availableSpace, BorderGradientAngle ?? 0);
paint.SetLinearGradient(gradientPoints.start, gradientPoints.end, BorderGradientColors);
return paint;
}
if (BorderColor.Hex != Colors.Transparent.Hex)
{
var paint = new SkPaint();
paint.SetSolidColor(BorderColor);
return paint;
}
return null;
}
private SkPaint? GetBackgroundPaint(Size availableSpace)
{
if (BackgroundGradientColors.Length > 0)
{
var paint = new SkPaint();
var gradientPoints = GetLinearGradientPositions(availableSpace, BackgroundGradientAngle ?? 0);
paint.SetLinearGradient(gradientPoints.start, gradientPoints.end, BackgroundGradientColors);
return paint;
}
if (BackgroundColor.Hex != Colors.Transparent.Hex)
{
var paint = new SkPaint();
paint.SetSolidColor(BackgroundColor);
return paint;
}
return null;
}
private SkRoundedRect GetPrimaryBorderRect(Size availableSpace)
{
return new SkRoundedRect
{
Rect = new SkRect
{
Left = 0,
Top = 0,
Right = availableSpace.Width,
Bottom = availableSpace.Height
},
TopLeftRadius = new SkPoint(BorderRadiusTopLeft, BorderRadiusTopLeft),
TopRightRadius = new SkPoint(BorderRadiusTopRight, BorderRadiusTopRight),
BottomLeftRadius = new SkPoint(BorderRadiusBottomLeft, BorderRadiusBottomLeft),
BottomRightRadius = new SkPoint(BorderRadiusBottomRight, BorderRadiusBottomRight)
};
}
private SkRoundedRect GetBorderRectExpandedWithBorderThickness(Size availableSpace, float borderThicknessExpansionFactor)
{
var primaryRect = GetPrimaryBorderRect(availableSpace);
return ExpandRoundedRect(
primaryRect,
borderThicknessExpansionFactor * BorderLeft,
borderThicknessExpansionFactor * BorderTop,
borderThicknessExpansionFactor * BorderRight,
borderThicknessExpansionFactor * BorderBottom);
}
private SkRoundedRect GetOuterRect(Size availableSpace)
{
return GetBorderRectExpandedWithBorderThickness(availableSpace, BorderAlignment!.Value);
}
private SkRoundedRect GetInnerRect(Size availableSpace)
{
return GetBorderRectExpandedWithBorderThickness(availableSpace, BorderAlignment!.Value - 1f);
}
private SkRoundedRect ExpandRoundedRect(SkRoundedRect rect, float all)
{
return ExpandRoundedRect(rect, all, all, all, all);
}
private SkRoundedRect ExpandRoundedRect(SkRoundedRect input, float left, float top, float right, float bottom)
{
var rect = new SkRect
{
Left = input.Rect.Left - left,
Top = input.Rect.Top - top,
Right = input.Rect.Right + right,
Bottom = input.Rect.Bottom + bottom
};
var hasRoundedCorners =
input.TopLeftRadius.X > 0 ||
input.TopRightRadius.X > 0 ||
input.BottomLeftRadius.X > 0 ||
input.BottomRightRadius.X > 0;
if (!hasRoundedCorners)
return new SkRoundedRect { Rect = rect };
return new SkRoundedRect
{
Rect = rect,
TopLeftRadius = new SkPoint
{
X = Math.Max(0, input.TopLeftRadius.X + left),
Y = Math.Max(0, input.TopLeftRadius.Y + top)
},
TopRightRadius = new SkPoint
{
X = Math.Max(0, input.TopRightRadius.X + right),
Y = Math.Max(0, input.TopRightRadius.Y + top)
},
BottomLeftRadius = new SkPoint
{
X = Math.Max(0, input.BottomLeftRadius.X + left),
Y = Math.Max(0, input.BottomLeftRadius.Y + bottom)
},
BottomRightRadius = new SkPoint
{
X = Math.Max(0, input.BottomRightRadius.X + right),
Y = Math.Max(0, input.BottomRightRadius.Y + bottom)
}
};
}
internal IEnumerable<(string Type, string? Hint)> GetCompanionCustomContent()
{
// shadow
if (Shadow != null)
yield return ("Shadow", null);
// rounded corners
if (HasRoundedCorners)
{
if (HasUniformRoundedCorners)
yield return ("Border", $"R={BorderRadiusTopLeft}");
else
yield return ("Border", $"TL={BorderRadiusTopLeft} TR={BorderRadiusTopRight} BL={BorderRadiusBottomLeft} BR={BorderRadiusBottomRight}");
}
// border
if (HasBorder)
{
var color = BorderGradientColors.Any() ? "gradient" : BorderColor.ToString();
if (HasUniformBorder)
yield return ("Border", $"A={BorderLeft} C={color}");
else
yield return ("Border", $"L={BorderLeft} T={BorderTop} R={BorderRight} B={BorderBottom} C={color}");
}
// background
if (BackgroundGradientColors.Length > 0)
yield return ("Background", $"Gradient with {BackgroundGradientColors.Length} colors");
else if (BackgroundColor.Hex != Colors.Transparent.Hex)
yield return ("Background", BackgroundColor);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/SvgImage.cs
================================================
using System;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements;
internal sealed class SvgImage : Element, IStateful, IDisposable
{
public Infrastructure.SvgImage Image { get; set; }
~SvgImage()
{
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
if (Image != null && !Image.IsShared)
Image?.Dispose();
GC.SuppressFinalize(this);
}
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
return SpacePlan.FullRender(Size.Zero);
}
internal override void Draw(Size availableSpace)
{
if (IsRendered)
return;
var (widthScale, heightScale) = Image.SkSvgImage.CalculateSpaceScale(availableSpace);
Canvas.Save();
Canvas.Scale(widthScale, heightScale);
Canvas.DrawSvg(Image.SkSvgImage, availableSpace);
Canvas.Restore();
IsRendered = true;
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false) => IsRendered = false;
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
}
================================================
FILE: Source/QuestPDF/Elements/SvgPath.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Elements;
internal sealed class SvgPath : Element, IStateful
{
public string Path { get; set; } = string.Empty;
public Color FillColor { get; set; } = Colors.Black;
internal override SpacePlan Measure(Size availableSpace)
{
if (IsRendered)
return SpacePlan.Empty();
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
return SpacePlan.FullRender(Size.Zero);
}
internal override void Draw(Size availableSpace)
{
if (IsRendered)
return;
Canvas.DrawSvgPath(Path, FillColor);
IsRendered = true;
}
#region IStateful
private bool IsRendered { get; set; }
public void ResetState(bool hardReset = false) => IsRendered = false;
public object GetState() => IsRendered;
public void SetState(object state) => IsRendered = (bool) state;
#endregion
}
================================================
FILE: Source/QuestPDF/Elements/Table/DynamicDictionary.cs
================================================
using System.Collections.Generic;
using System.Linq;
namespace QuestPDF.Elements.Table
{
///
/// This dictionary allows to access key that does not exist.
/// Instead of throwing an exception, it returns a default value.
///
internal sealed class DynamicDictionary
{
private TValue Default { get; }
private IDictionary Dictionary { get; } = new Dictionary();
public DynamicDictionary()
{
}
public DynamicDictionary(TValue defaultValue)
{
Default = defaultValue;
}
public TValue this[TKey key]
{
get => Dictionary.TryGetValue(key, out var value) ? value : Default;
set => Dictionary[key] = value;
}
public List> Items => Dictionary.ToList();
}
}
================================================
FILE: Source/QuestPDF/Elements/Table/ITableCellContainer.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
public interface ITableCellContainer : IContainer
{
}
}
================================================
FILE: Source/QuestPDF/Elements/Table/Table.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
internal sealed class Table : Element, IStateful, IContentDirectionAware, ISemanticAware
{
// configuration
public List Columns { get; set; } = new();
public List Cells { get; set; } = new();
public bool ExtendLastCellsToTableBottom { get; set; }
public ContentDirection ContentDirection { get; set; }
// cache
private bool CacheInitialized { get; set; }
private bool HasRelativeColumns { get; set; }
private int LastRowIndex { get; set; }
private int RowsCount { get; set; }
private int MaxRow { get; set; }
private int MaxRowSpan { get; set; }
// cache that stores all cells
// first index: row number
// inner table: list of all cells that ends at the corresponding row
private TableCell[][] CellsCache { get; set; }
private bool IsRendered => CurrentRow > LastRowIndex;
internal override IEnumerable GetChildren()
{
return Cells;
}
private void Initialize()
{
if (CacheInitialized)
return;
HasRelativeColumns = Columns.Any(x => x.RelativeSize > 0);
LastRowIndex = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
BuildCache();
CacheInitialized = true;
}
private void BuildCache()
{
if (CellsCache != null)
return;
if (Cells.Count == 0)
{
MaxRow = 0;
MaxRowSpan = 1;
CellsCache = Array.Empty();
return;
}
var groups = Cells
.GroupBy(x => x.Row + x.RowSpan - 1)
.ToDictionary(x => x.Key, x => x.OrderBy(x => x.Column).ToArray());
MaxRow = groups.Max(x => x.Key);
MaxRowSpan = Cells.Max(x => x.RowSpan);
CellsCache = Enumerable
.Range(0, MaxRow + 1)
.Select(x => groups.TryGetValue(x, out var value) ? value : Array.Empty())
.ToArray();
}
internal override SpacePlan Measure(Size availableSpace)
{
Initialize();
if (!Cells.Any())
return SpacePlan.Empty();
if (IsRendered)
return SpacePlan.Empty();
if (HasRelativeColumns && availableSpace.Width < Size.Epsilon)
return SpacePlan.Wrap("Insufficient space to render columns of relative size.");
UpdateColumnsWidth(availableSpace.Width);
var renderingCommands = PlanLayout(availableSpace);
if (!renderingCommands.Any())
return SpacePlan.Wrap("Insufficient space to render (even partially) a single row.");
var width = Columns.Sum(x => x.Width);
var height = renderingCommands.Max(x => x.Offset.Y + x.Size.Height);
var tableSize = new Size(width, height);
if (tableSize.Width > availableSpace.Width + Size.Epsilon)
return SpacePlan.Wrap("The content requires more horizontal space than available.");
return CalculateCurrentRow(renderingCommands) > LastRowIndex
? SpacePlan.FullRender(tableSize)
: SpacePlan.PartialRender(tableSize);
}
internal override void Draw(Size availableSpace)
{
Initialize();
RegisterSemanticTree();
if (IsRendered)
return;
UpdateColumnsWidth(availableSpace.Width);
var renderingCommands = PlanLayout(availableSpace);
foreach (var command in renderingCommands.OrderBy(x => x.Cell.ZIndex))
{
if (command.Measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
command.Cell.IsRendered = true;
if (command.Measurement.Type == SpacePlanType.Wrap)
continue;
var offset = ContentDirection == ContentDirection.LeftToRight
? command.Offset
: new Position(availableSpace.Width - command.Offset.X - command.Size.Width, command.Offset.Y);
Canvas.Translate(offset);
command.Cell.Draw(command.Size);
Canvas.Translate(offset.Reverse());
}
CurrentRow = CalculateCurrentRow(renderingCommands);
}
private int CalculateCurrentRow(ICollection commands)
{
if (!commands.Any())
return CurrentRow;
var notRenderedCells = commands
.Where(x => !x.Cell.IsRendered)
.Where(x => x.Measurement.Type is SpacePlanType.Wrap or SpacePlanType.PartialRender)
.ToList();
if (notRenderedCells.Any())
return notRenderedCells.Min(x => x.Cell.Row + x.Cell.RowSpan - 1);
return commands.Max(x => x.Cell.Row + x.Cell.RowSpan - 1) + 1;
}
private void UpdateColumnsWidth(float availableWidth)
{
var constantWidth = Columns.Sum(x => x.ConstantSize);
var relativeWidth = Columns.Sum(x => x.RelativeSize);
var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
foreach (var column in Columns)
{
column.Width = column.ConstantSize + column.RelativeSize * widthPerRelativeUnit;
}
}
private ICollection PlanLayout(Size availableSpace)
{
var columnOffsets = GetColumnLeftOffsets(Columns);
var commands = GetRenderingCommands();
if (!commands.Any())
return commands;
if (ExtendLastCellsToTableBottom)
{
var tableHeight = commands.Max(cell => cell.Offset.Y + cell.Size.Height);
AdjustLastCellSizes(tableHeight, commands);
}
return commands;
static float[] GetColumnLeftOffsets(IList columns)
{
var cellOffsets = new float[columns.Count + 1];
cellOffsets[0] = 0;
foreach (var column in Enumerable.Range(1, cellOffsets.Length - 1))
cellOffsets[column] = columns[column - 1].Width + cellOffsets[column - 1];
return cellOffsets;
}
ICollection GetRenderingCommands()
{
var rowBottomOffsets = new DynamicDictionary();
var commands = new List();
var cellsToTry = Enumerable
.Range(CurrentRow, MaxRow - CurrentRow + 1)
.SelectMany(x => CellsCache[x]);
var currentRow = CurrentRow;
var maxRenderingRow = RowsCount;
foreach (var cell in cellsToTry)
{
// update position of previous row
if (cell.Row > currentRow)
{
rowBottomOffsets[currentRow] = Math.Max(rowBottomOffsets[currentRow], rowBottomOffsets[currentRow - 1]);
if (rowBottomOffsets[currentRow - 1] > availableSpace.Height + Size.Epsilon)
break;
foreach (var row in Enumerable.Range(currentRow + 1, cell.Row - (currentRow + 1)))
rowBottomOffsets[row] = Math.Max(rowBottomOffsets[row - 1], rowBottomOffsets[row]);
currentRow = cell.Row;
}
// cell visibility optimizations
if (cell.Row > maxRenderingRow + MaxRowSpan)
break;
// calculate cell position / size
var topOffset = rowBottomOffsets[cell.Row - 1];
var availableWidth = GetCellWidth(cell);
var availableHeight = availableSpace.Height - topOffset;
var availableCellSize = new Size(availableWidth, availableHeight);
var cellSize = cell.Measure(availableCellSize);
// corner case: if cell within the row is not fully rendered, do not attempt to render next row
if (cellSize.Type == SpacePlanType.PartialRender)
{
maxRenderingRow = Math.Min(maxRenderingRow, cell.Row + cell.RowSpan - 1);
}
// corner case: if cell within the row want to wrap to the next page, do not attempt to render this row
if (cellSize.Type == SpacePlanType.Wrap)
{
maxRenderingRow = Math.Min(maxRenderingRow, cell.Row - 1);
continue;
}
// update position of the last row that cell occupies
var bottomRow = cell.Row + cell.RowSpan - 1;
rowBottomOffsets[bottomRow] = Math.Max(rowBottomOffsets[bottomRow], topOffset + cellSize.Height);
// accept cell to be rendered
commands.Add(new TableCellRenderingCommand()
{
Cell = cell,
Measurement = cellSize,
Size = new Size(availableWidth, cellSize.Height),
Offset = new Position(columnOffsets[cell.Column - 1], topOffset)
});
}
if (!commands.Any())
return commands;
var maxRow = commands.Select(x => x.Cell).Max(x => x.Row + x.RowSpan);
foreach (var row in Enumerable.Range(CurrentRow, maxRow - CurrentRow))
rowBottomOffsets[row] = Math.Max(rowBottomOffsets[row - 1], rowBottomOffsets[row]);
AdjustCellSizes(commands, rowBottomOffsets);
// corner case: reject cell if other cells within the same row are rejected
return commands.Where(x => x.Cell.Row <= maxRenderingRow).ToList();
}
// corner sase: if two cells end up on the same row (a.Row + a.RowSpan = b.Row + b.RowSpan),
// bottom edges of their bounding boxes should be at the same level
static void AdjustCellSizes(ICollection commands, DynamicDictionary rowBottomOffsets)
{
foreach (var command in commands)
{
var lastRow = command.Cell.Row + command.Cell.RowSpan - 1;
var height = rowBottomOffsets[lastRow] - command.Offset.Y;
command.Size = new Size(command.Size.Width, height);
command.Offset = new Position(command.Offset.X, rowBottomOffsets[command.Cell.Row - 1]);
}
}
// corner sase: all cells, that are last ones in their respective columns, should take all remaining space
static void AdjustLastCellSizes(float tableHeight, ICollection commands)
{
var columnsCount = commands.Select(x => x.Cell).Max(x => x.Column + x.ColumnSpan - 1);
foreach (var column in Enumerable.Range(1, columnsCount))
{
var lastCellInColumn = commands
.Where(x => x.Cell.Column <= column && column < x.Cell.Column + x.Cell.ColumnSpan)
.OrderByDescending(x => x.Cell.Row + x.Cell.RowSpan)
.FirstOrDefault();
if (lastCellInColumn == null)
continue;
lastCellInColumn.Size = new Size(lastCellInColumn.Size.Width, tableHeight - lastCellInColumn.Offset.Y);
}
}
float GetCellWidth(TableCell cell)
{
return columnOffsets[cell.Column + cell.ColumnSpan - 1] - columnOffsets[cell.Column - 1];
}
}
#region IStateful
private int CurrentRow { get; set; }
// state is also stored in TableCell instances
public struct TableState
{
public bool[] CellsRenderingState;
public int CurrentRow;
}
public void ResetState(bool hardReset = false)
{
foreach (var x in Cells)
x.IsRendered = false;
CurrentRow = 1;
}
public object GetState()
{
var cellsRenderingState = new bool[Cells.Count];
for (var i = 0; i < Cells.Count; i++)
cellsRenderingState[i] = Cells[i].IsRendered;
return new TableState
{
CellsRenderingState = cellsRenderingState,
CurrentRow = CurrentRow
};
}
public void SetState(object state)
{
var tableState = (TableState) state;
for (var i = 0; i < Cells.Count; i++)
Cells[i].IsRendered = tableState.CellsRenderingState[i];
CurrentRow = tableState.CurrentRow;
}
#endregion
#region Semantic
internal enum TablePartType
{
Header,
Body,
Footer
}
internal bool EnableAutomatedSemanticTagging { get; set; }
private bool IsSemanticTaggingApplied { get; set; }
public SemanticTreeManager? SemanticTreeManager { get; set; } = new();
internal bool TableRequiresAdvancedHeaderTagging { get; set; }
internal TablePartType PartType { get; set; }
public List HeaderCells { get; set; } = [];
private void RegisterSemanticTree()
{
if (SemanticTreeManager == null)
return;
if (SemanticTreeManager.IsCurrentContentArtifact())
return;
if (!EnableAutomatedSemanticTagging)
return;
if (IsSemanticTaggingApplied)
return;
IsSemanticTaggingApplied = true;
foreach (var tableRow in Cells.GroupBy(x => x.Row))
{
var rowSemanticTreeNode = new SemanticTreeNode()
{
NodeId = SemanticTreeManager.GetNextNodeId(),
Type = "TR"
};
SemanticTreeManager.AddNode(rowSemanticTreeNode);
SemanticTreeManager.PushOnStack(rowSemanticTreeNode);
foreach (var tableCell in tableRow.OrderBy(x => x.Column))
{
tableCell.CreateProxy(x => new SemanticTag
{
SemanticTreeManager = SemanticTreeManager,
Canvas = Canvas,
TagType = "TD",
Child = x
});
if (tableCell.Child is not SemanticTag semanticTag)
continue;
if (PartType is TablePartType.Header || tableCell.IsSemanticHorizontalHeader)
semanticTag.TagType = "TH";
semanticTag.RegisterCurrentSemanticNode();
tableCell.SemanticNodeId = semanticTag.SemanticTreeNode!.NodeId;
AssignCellAttributesForColumnAndRowSpans(tableCell, semanticTag);
}
SemanticTreeManager.PopStack();
}
AssignCellAttributesForHeaderCellRoles();
static void AssignCellAttributesForColumnAndRowSpans(TableCell tableCell, SemanticTag semanticTag)
{
if (tableCell.ColumnSpan > 1)
{
semanticTag.SemanticTreeNode.Attributes.Add(new SemanticTreeNode.Attribute
{
Owner = "Table",
Name = "ColSpan",
Value = tableCell.ColumnSpan
});
}
if (tableCell.RowSpan > 1)
{
semanticTag.SemanticTreeNode.Attributes.Add(new SemanticTreeNode.Attribute
{
Owner = "Table",
Name = "RowSpan",
Value = tableCell.RowSpan
});
}
}
void AssignCellAttributesForHeaderCellRoles()
{
if (PartType is TablePartType.Footer)
return;
if (TableRequiresAdvancedHeaderTagging)
{
AssignCellAttributesForHeaderCellRolesOfComplexTables();
}
else
{
AssignCellAttributesForHeaderCellRolesOfSimpleTables();
}
}
void AssignCellAttributesForHeaderCellRolesOfSimpleTables()
{
foreach (var tableCell in Cells)
{
if (tableCell.Child is not SemanticTag semanticTag)
continue;
if (semanticTag.TagType != "TH")
continue;
var scopeValue = (PartType is TablePartType.Header, tableCell.IsSemanticHorizontalHeader) switch
{
(true, true) => "Both",
(true, false) => "Column",
(false, true) => "Row",
(false, false) => null
};
if (scopeValue == null)
continue;
semanticTag.SemanticTreeNode.Attributes.Add(new SemanticTreeNode.Attribute
{
Owner = "Table",
Name = "Scope",
Value = scopeValue
});
}
}
void AssignCellAttributesForHeaderCellRolesOfComplexTables()
{
var semanticHorizontalHeaders = Cells
.Where(x => x.IsSemanticHorizontalHeader)
.ToList();
foreach (var tableCell in Cells)
{
if (tableCell.Child is not SemanticTag semanticTag)
continue;
var relatedHeaders = GetRelatedHeadersFor(tableCell).ToArray();
if (!relatedHeaders.Any())
continue;
semanticTag.SemanticTreeNode!.Attributes.Add(new SemanticTreeNode.Attribute
{
Owner = "Table",
Name = "Headers",
Value = relatedHeaders
});
}
IEnumerable GetRelatedHeadersFor(TableCell cell)
{
var isHeader = PartType == TablePartType.Header;
var headerCells = (isHeader ? Cells : HeaderCells).AsEnumerable();
if (isHeader)
headerCells = headerCells.Where(x => x.Row < cell.Row);
var relatedVerticalHeaders = headerCells
.Where(x => x.Column < cell.Column + cell.ColumnSpan && cell.Column < x.Column + x.ColumnSpan)
.Select(x => x.SemanticNodeId);
if (isHeader)
return relatedVerticalHeaders;
var relatedHorizontalHeaders = semanticHorizontalHeaders
.Where(x => x.Column < cell.Column)
.Where(x => x.Row < cell.Row + cell.RowSpan && cell.Row < x.Row + x.RowSpan)
.Select(x => x.SemanticNodeId);
return relatedVerticalHeaders.Concat(relatedHorizontalHeaders);
}
}
}
public static bool DoesTableBodyRequireExtendedHeaderTagging(ICollection headerCells, ICollection bodyCells)
{
return ContainsSpanningCells(headerCells) || ContainsSpanningCells(bodyCells);
static bool ContainsSpanningCells(IEnumerable cells) =>
cells.Any(x => x.RowSpan > 1 || x.ColumnSpan > 1);
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Elements/Table/TableCell.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
internal sealed class TableCell : ContainerElement, ITableCellContainer
{
public int Row { get; set; } = 0;
public int RowSpan { get; set; } = 1;
public int Column { get; set; } = 0;
public int ColumnSpan { get; set; } = 1;
public int ZIndex { get; set; }
public bool IsSemanticHorizontalHeader { get; set; }
public int SemanticNodeId { get; set; }
public bool IsRendered { get; set; }
}
}
================================================
FILE: Source/QuestPDF/Elements/Table/TableCellRenderingCommand.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
internal sealed class TableCellRenderingCommand
{
public TableCell Cell { get; set; }
public SpacePlan Measurement { get; set; }
public Size Size { get; set; }
public Position Offset { get; set; }
}
}
================================================
FILE: Source/QuestPDF/Elements/Table/TableColumnDefinition.cs
================================================
namespace QuestPDF.Elements.Table
{
internal sealed class TableColumnDefinition
{
public float ConstantSize { get; }
public float RelativeSize { get; }
internal float Width { get; set; }
public TableColumnDefinition(float constantSize, float relativeSize)
{
ConstantSize = constantSize;
RelativeSize = relativeSize;
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Table/TableLayoutPlanner.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuestPDF.Elements.Table
{
static class TableLayoutPlanner
{
public static void PlanCellPositions(this Table table)
{
PlanCellPositions(table.Columns.Count, table.Cells);
}
private static void PlanCellPositions(int columnsCount, ICollection cells)
{
var cellsWindow = new List();
(int x, int y) currentLocation = (1, 1);
var zIndex = 0;
foreach (var cell in cells)
{
cell.ZIndex = zIndex;
zIndex++;
if (cellsWindow.Count > Math.Max(columnsCount, 16))
{
cellsWindow = cellsWindow
.Where(x => x.Row + x.RowSpan > currentLocation.y)
.ToList();
}
SetPartialLocation(cell);
if (cell.HasLocation())
{
cellsWindow.Add(cell);
currentLocation = (cell.Column, cell.Row);
continue;
}
foreach (var location in GenerateCoordinates(columnsCount, currentLocation))
{
if (location.x + cell.ColumnSpan - 1 > columnsCount)
continue;
cell.Column = location.x;
cell.Row = location.y;
if (cell.CollidesWithAnyOf(cellsWindow))
continue;
cellsWindow.Add(cell);
currentLocation = (cell.Column, cell.Row);
break;
}
}
}
private static IEnumerable<(int x, int y)> GenerateCoordinates(int columnsCount, (int x, int y) startPosition)
{
if (startPosition.x > columnsCount)
throw new ArgumentException();
foreach (var x in Enumerable.Range(startPosition.x, columnsCount - startPosition.x + 1))
yield return (x, startPosition.y);
foreach (var y in Enumerable.Range(startPosition.y + 1, 1_000_000))
foreach (var x in Enumerable.Range(1, columnsCount))
yield return (x, y);
}
private static bool CollidesWith(this TableCell cell, TableCell neighbour)
{
return cell.Column < neighbour.Column + neighbour.ColumnSpan &&
cell.Column + cell.ColumnSpan > neighbour.Column &&
cell.Row < neighbour.Row + neighbour.RowSpan &&
cell.RowSpan + cell.Row > neighbour.Row;
}
private static bool CollidesWithAnyOf(this TableCell cell, ICollection neighbours)
{
return neighbours.Any(cell.CollidesWith);
}
private static void SetPartialLocation(this TableCell cell)
{
if (cell.Row == default && cell.Column == default)
return;
if (cell.Row == default)
cell.Row = 1;
if (cell.Column == default)
cell.Column = 1;
}
private static bool HasLocation(this TableCell cell)
{
return cell.Row != 0 && cell.Column != 0;
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Table/TableLayoutValidator.cs
================================================
using System.Collections.Generic;
using QuestPDF.Drawing.Exceptions;
namespace QuestPDF.Elements.Table
{
static class TableLayoutValidator
{
public static void ValidateCellPositions(this Table table)
{
ValidateCellPositions(table.Columns.Count, table.Cells);
}
private static void ValidateCellPositions(int columnsCount, ICollection cells)
{
const string prefix = "Detected issue in table cells configuration.";
foreach (var cell in cells)
{
if (cell.Column < 1)
throw new DocumentComposeException($"{prefix} A cell column position should be greater or equal to 1. Got {cell.Column}.");
if (cell.Row < 1)
throw new DocumentComposeException($"{prefix} A cell row position should be greater or equal to 1. Got {cell.Row}.");
if (cell.ColumnSpan < 1)
throw new DocumentComposeException($"{prefix} A cell must span at least one column. Got {cell.ColumnSpan}.");
if (cell.RowSpan < 1)
throw new DocumentComposeException($"{prefix} A cell must span at least one row. Got {cell.RowSpan}.");
if (cell.Column > columnsCount)
throw new DocumentComposeException($"{prefix} Cell starts at column that does not exist. Cell details: {GetCellDetails(cell)}.");
if (cell.Column + cell.ColumnSpan - 1 > columnsCount)
throw new DocumentComposeException($"{prefix} Table cell location is incorrect. Cell spans over columns that do not exist. Cell details: {GetCellDetails(cell)}.");
}
string GetCellDetails(TableCell cell)
{
return $"Row {cell.Row}, Column {cell.Column}, RowSpan {cell.RowSpan}, ColumnSpan {cell.ColumnSpan}";
}
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/Items/ITextBlockItem.cs
================================================
namespace QuestPDF.Elements.Text.Items
{
internal interface ITextBlockItem
{
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/Items/TextBlockElement.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal sealed class TextBlockElement : ITextBlockItem
{
public Element Element { get; set; } = Empty.Instance;
public Size ElementSize { get; set; } = Size.Zero;
public TextInjectedElementAlignment Alignment { get; set; } = TextInjectedElementAlignment.AboveBaseline;
public int ParagraphBlockIndex { get; set; }
public void ConfigureElement(IPageContext pageContext, IDrawingCanvas canvas)
{
Element.VisitChildren(x => (x as IStateful)?.ResetState());
Element.InjectDependencies(pageContext, canvas);
}
public void UpdateElementSize()
{
ElementSize = Element.Measure(Size.Max);
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/Items/TextBlockHyperlink.cs
================================================
namespace QuestPDF.Elements.Text.Items
{
internal sealed class TextBlockHyperlink : TextBlockSpan
{
public string Url { get; set; }
public int ParagraphBeginIndex { get; set; }
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs
================================================
using System;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal sealed class TextBlockPageNumber : TextBlockSpan
{
public const string PageNumberPlaceholder = "123";
public Func Source { get; set; } = _ => PageNumberPlaceholder;
public void UpdatePageNumberText(IPageContext context)
{
Text = Source(context) ?? PageNumberPlaceholder;
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/Items/TextBlockParagraphSpacing.cs
================================================
namespace QuestPDF.Elements.Text.Items;
internal sealed class TextBlockParagraphSpacing : ITextBlockItem
{
public float Width { get; }
public float Height { get; }
public TextBlockParagraphSpacing(float width, float height)
{
Width = width;
Height = height;
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/Items/TextBlockSectionLink.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal sealed class TextBlockSectionLink : TextBlockSpan
{
public string SectionName { get; set; }
public int ParagraphBeginIndex { get; set; }
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/Items/TextBlockSpan.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSpan : ITextBlockItem
{
public string Text { get; set; }
public TextStyle Style { get; set; } = TextStyle.Default;
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/SkParagraphBuilderPoolManager.cs
================================================
using System;
using System.Collections.Concurrent;
using QuestPDF.Drawing;
using QuestPDF.Skia.Text;
namespace QuestPDF.Elements.Text;
internal static class SkParagraphBuilderPoolManager
{
private static ConcurrentDictionary> ObjectPool { get; } = new();
public static SkParagraphBuilder Get(ParagraphStyle style)
{
var specificPool = GetPool(style);
if (specificPool.TryTake(out var builder))
return builder;
var fontCollection = SkFontCollection.Create(FontManager.TypefaceProvider, FontManager.CurrentFontManager);
return SkParagraphBuilder.Create(style, fontCollection);
}
public static void Return(SkParagraphBuilder builder)
{
builder.Reset();
var specificPool = GetPool(builder.Style);
specificPool.Add(builder);
}
private static ConcurrentBag GetPool(ParagraphStyle style)
{
return ObjectPool.GetOrAdd(style, _ => new ConcurrentBag());
}
}
================================================
FILE: Source/QuestPDF/Elements/Text/TextBlock.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
using QuestPDF.Skia.Text;
namespace QuestPDF.Elements.Text
{
internal sealed class TextBlock : Element, IStateful, IContentDirectionAware, IDisposable
{
// content
public List Items { get; set; } = new();
// configuration
public TextHorizontalAlignment? Alignment { get; set; }
public ContentDirection ContentDirection { get; set; }
public int? LineClamp { get; set; }
public string LineClampEllipsis { get; set; }
public float ParagraphSpacing { get; set; }
public float ParagraphFirstLineIndentation { get; set; }
public TextStyle DefaultTextStyle { get; set; } = TextStyle.Default;
// cache
private bool RebuildParagraphForEveryPage { get; set; }
private bool AreParagraphMetricsValid { get; set; }
private bool AreParagraphItemsTransformedWithSpacingAndIndentation { get; set; }
private SkSize[] LineMetrics { get; set; }
private float WidthForLineMetricsCalculation { get; set; }
private float MaximumWidth { get; set; }
private SkRect[] PlaceholderPositions { get; set; }
private bool? ContainsOnlyWhiteSpace { get; set; }
// native objects
private SkParagraph Paragraph { get; set; }
internal bool ClearInternalCacheAfterFullRender { get; set; } = true;
public string Text => string.Join(" ", Items.OfType().Select(x => x.Text));
~TextBlock()
{
if (Paragraph == null)
return;
this.WarnThatFinalizerIsReached();
Dispose();
}
public void Dispose()
{
Paragraph?.Dispose();
foreach (var textBlockElement in Items.OfType())
textBlockElement.Element.ReleaseDisposableChildren();
GC.SuppressFinalize(this);
}
internal override SpacePlan Measure(Size availableSpace)
{
if (Items.Count == 0)
return SpacePlan.Empty();
if (IsRendered)
return SpacePlan.Empty();
if (availableSpace.IsNegative())
return SpacePlan.Wrap("The available space is negative.");
// if the text block does not contain any items, or all items are null, return SpacePlan.Empty
// but if the text block contains only whitespace, return SpacePlan.FullRender with zero width and font-based height
ContainsOnlyWhiteSpace ??= CheckIfContainsOnlyWhiteSpace();
if (ContainsOnlyWhiteSpace == true)
{
var requiredHeight = MeasureHeightOfParagraphContainingOnlyWhiteSpace();
return requiredHeight < availableSpace.Height + Size.Epsilon
? SpacePlan.FullRender(0, requiredHeight)
: SpacePlan.Wrap("The available vertical space is not sufficient to render even a single line of text.");
}
if (availableSpace.Width < Size.Epsilon || availableSpace.Height < Size.Epsilon)
return SpacePlan.Wrap("The available space is not sufficient to render even a single line of text.");
Initialize();
CalculateParagraphMetrics(availableSpace);
if (availableSpace.Width < MaximumWidth - Size.Epsilon)
return SpacePlan.Wrap($"The available space is not sufficient to render even a single character.");
if (MaximumWidth == 0)
return SpacePlan.FullRender(Size.Zero);
var totalHeight = 0f;
var totalLines = 0;
for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
{
var lineMetric = LineMetrics[lineIndex];
var newTotalHeight = totalHeight + lineMetric.Height;
if (newTotalHeight > availableSpace.Height + Size.Epsilon)
break;
totalHeight = newTotalHeight;
totalLines++;
}
if (totalLines == 0)
return SpacePlan.Wrap("The available space is not sufficient to render even a single line of text.");
var requiredArea = new Size(
Math.Min(MaximumWidth, availableSpace.Width),
Math.Min(totalHeight, availableSpace.Height));
if (CurrentLineIndex + totalLines < LineMetrics.Length)
return SpacePlan.PartialRender(requiredArea);
return SpacePlan.FullRender(requiredArea);
}
internal override void Draw(Size availableSpace)
{
if (Items.Count == 0)
return;
if (IsRendered)
return;
if (ContainsOnlyWhiteSpace == true)
return;
CalculateParagraphMetrics(availableSpace);
if (MaximumWidth == 0)
return;
var (linesToDraw, takenHeight) = DetermineLinesToDraw();
DrawParagraph();
CurrentLineIndex += linesToDraw;
CurrentTopOffset += takenHeight;
if (CurrentLineIndex == LineMetrics.Length)
IsRendered = true;
if (IsRendered && ClearInternalCacheAfterFullRender)
{
Paragraph?.Dispose();
Paragraph = null;
}
return;
(int linesToDraw, float takenHeight) DetermineLinesToDraw()
{
var linesToDraw = 0;
var takenHeight = 0f;
for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
{
var lineMetric = LineMetrics[lineIndex];
var newTotalHeight = takenHeight + lineMetric.Height;
if (newTotalHeight > availableSpace.Height + Size.Epsilon)
break;
takenHeight = newTotalHeight;
linesToDraw++;
}
return (linesToDraw, takenHeight);
}
void DrawParagraph()
{
var takesMultiplePages = linesToDraw != LineMetrics.Length;
if (takesMultiplePages)
{
Canvas.Save();
Canvas.Translate(new Position(0, -CurrentTopOffset));
}
Canvas.DrawParagraph(Paragraph, CurrentLineIndex, CurrentLineIndex + linesToDraw - 1);
if (takesMultiplePages)
Canvas.ClipRectangle(new SkRect(0, CurrentTopOffset, availableSpace.Width, takenHeight + CurrentTopOffset));
DrawInjectedElements();
DrawHyperlinks();
DrawSectionLinks();
if (takesMultiplePages)
Canvas.Restore();
}
void DrawInjectedElements()
{
foreach (var textBlockElement in Items.OfType())
{
var placeholder = PlaceholderPositions[textBlockElement.ParagraphBlockIndex];
textBlockElement.ConfigureElement(PageContext, Canvas);
var offset = new Position(placeholder.Left, placeholder.Top);
if (!IsPositionVisible(offset))
continue;
Canvas.Translate(offset);
textBlockElement.Element.Draw(new Size(placeholder.Width, placeholder.Height));
Canvas.Translate(offset.Reverse());
}
}
void DrawHyperlinks()
{
foreach (var hyperlink in Items.OfType())
{
var positions = Paragraph.GetTextRangePositions(hyperlink.ParagraphBeginIndex, hyperlink.ParagraphBeginIndex + hyperlink.Text.Length);
foreach (var position in positions)
{
var offset = new Position(position.Left, position.Top);
if (!IsPositionVisible(offset))
continue;
Canvas.Translate(offset);
Canvas.DrawHyperlink(new Size(position.Width, position.Height), hyperlink.Url, hyperlink.Text);
Canvas.Translate(offset.Reverse());
}
}
}
void DrawSectionLinks()
{
foreach (var sectionLink in Items.OfType())
{
var positions = Paragraph.GetTextRangePositions(sectionLink.ParagraphBeginIndex, sectionLink.ParagraphBeginIndex + sectionLink.Text.Length);
var targetName = PageContext.GetDocumentLocationName(sectionLink.SectionName);
foreach (var position in positions)
{
var offset = new Position(position.Left, position.Top);
if (!IsPositionVisible(offset))
continue;
Canvas.Translate(offset);
Canvas.DrawSectionLink(new Size(position.Width, position.Height), targetName, sectionLink.Text);
Canvas.Translate(offset.Reverse());
}
}
}
bool IsPositionVisible(Position position)
{
return CurrentTopOffset <= position.Y || position.Y <= CurrentTopOffset + takenHeight;
}
}
private void Initialize()
{
if (Paragraph != null && !RebuildParagraphForEveryPage)
return;
if (!AreParagraphItemsTransformedWithSpacingAndIndentation)
{
Items = ApplyParagraphSpacingToTextBlockItems().ToList();
AreParagraphItemsTransformedWithSpacingAndIndentation = true;
}
RebuildParagraphForEveryPage = Items.Any(x => x is TextBlockPageNumber);
BuildParagraph();
AreParagraphMetricsValid = false;
}
private void BuildParagraph()
{
Alignment ??= TextHorizontalAlignment.Start;
var paragraphStyle = new ParagraphStyle
{
Alignment = MapAlignment(Alignment.Value),
Direction = MapDirection(ContentDirection),
MaxLinesVisible = LineClamp ?? 1_000_000,
LineClampEllipsis = LineClampEllipsis
};
if (Paragraph != null)
{
Paragraph.Dispose();
Paragraph = null;
}
var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
try
{
Paragraph = CreateParagraph(builder);
}
finally
{
SkParagraphBuilderPoolManager.Return(builder);
}
static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
{
return alignment switch
{
TextHorizontalAlignment.Left => ParagraphStyleConfiguration.TextAlign.Left,
TextHorizontalAlignment.Center => ParagraphStyleConfiguration.TextAlign.Center,
TextHorizontalAlignment.Right => ParagraphStyleConfiguration.TextAlign.Right,
TextHorizontalAlignment.Justify => ParagraphStyleConfiguration.TextAlign.Justify,
TextHorizontalAlignment.Start => ParagraphStyleConfiguration.TextAlign.Start,
TextHorizontalAlignment.End => ParagraphStyleConfiguration.TextAlign.End,
_ => throw new Exception()
};
}
static ParagraphStyleConfiguration.TextDirection MapDirection(ContentDirection direction)
{
return direction switch
{
ContentDirection.LeftToRight => ParagraphStyleConfiguration.TextDirection.Ltr,
ContentDirection.RightToLeft => ParagraphStyleConfiguration.TextDirection.Rtl,
_ => throw new Exception()
};
}
static SkPlaceholderStyle.PlaceholderAlignment MapInjectedTextAlignment(TextInjectedElementAlignment alignment)
{
return alignment switch
{
TextInjectedElementAlignment.AboveBaseline => SkPlaceholderStyle.PlaceholderAlignment.AboveBaseline,
TextInjectedElementAlignment.BelowBaseline => SkPlaceholderStyle.PlaceholderAlignment.BelowBaseline,
TextInjectedElementAlignment.Top => SkPlaceholderStyle.PlaceholderAlignment.Top,
TextInjectedElementAlignment.Bottom => SkPlaceholderStyle.PlaceholderAlignment.Bottom,
TextInjectedElementAlignment.Middle => SkPlaceholderStyle.PlaceholderAlignment.Middle,
_ => throw new Exception()
};
}
SkParagraph CreateParagraph(SkParagraphBuilder builder)
{
var currentTextIndex = 0;
var currentBlockIndex = 0;
if (!Items.Any(x => x is TextBlockSpan))
builder.AddText("\u200B", DefaultTextStyle.GetSkTextStyle());
foreach (var textBlockItem in Items)
{
if (textBlockItem is TextBlockSpan textBlockSpan)
{
if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
textBlockPageNumber.UpdatePageNumberText(PageContext);
var textStyle = textBlockSpan.Style.GetSkTextStyle();
var text = textBlockSpan.Text?.Replace("\r", "") ?? "";
builder.AddText(text, textStyle);
currentTextIndex += text.Length;
}
else if (textBlockItem is TextBlockElement textBlockElement)
{
textBlockElement.ConfigureElement(PageContext, Canvas);
textBlockElement.UpdateElementSize();
textBlockElement.ParagraphBlockIndex = currentBlockIndex;
builder.AddPlaceholder(new SkPlaceholderStyle
{
Width = textBlockElement.ElementSize.Width,
Height = textBlockElement.ElementSize.Height,
Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
BaselineOffset = 0
});
currentTextIndex++;
currentBlockIndex++;
}
else if (textBlockItem is TextBlockParagraphSpacing spacing)
{
builder.AddPlaceholder(new SkPlaceholderStyle
{
Width = spacing.Width,
Height = spacing.Height,
Alignment = SkPlaceholderStyle.PlaceholderAlignment.Middle,
Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
BaselineOffset = 0
});
currentTextIndex++;
currentBlockIndex++;
}
}
return builder.CreateParagraph();
}
}
private IEnumerable ApplyParagraphSpacingToTextBlockItems()
{
if (ParagraphSpacing < Size.Epsilon && ParagraphFirstLineIndentation < Size.Epsilon)
return Items;
var result = new List();
AddParagraphFirstLineIndentation();
foreach (var textBlockItem in Items)
{
if (textBlockItem is not TextBlockSpan textBlockSpan)
{
result.Add(textBlockItem);
continue;
}
if (textBlockItem is TextBlockPageNumber)
{
result.Add(textBlockItem);
continue;
}
if (textBlockSpan.Text == "\n")
{
AddParagraphSpacing();
AddParagraphFirstLineIndentation();
continue;
}
var textFragments = textBlockSpan.Text.Split('\n');
foreach (var textFragment in textFragments)
{
AddClonedTextBlockSpanWithTextFragment(textBlockSpan, textFragment);
if (textFragment == textFragments.Last())
continue;
AddParagraphSpacing();
AddParagraphFirstLineIndentation();
}
}
return result;
void AddClonedTextBlockSpanWithTextFragment(TextBlockSpan originalSpan, string textFragment)
{
TextBlockSpan newItem;
if (originalSpan is TextBlockSectionLink textBlockSectionLink)
newItem = new TextBlockSectionLink { SectionName = textBlockSectionLink.SectionName };
else if (originalSpan is TextBlockHyperlink textBlockHyperlink)
newItem = new TextBlockHyperlink { Url = textBlockHyperlink.Url };
else if (originalSpan is TextBlockPageNumber textBlockPageNumber)
newItem = textBlockPageNumber;
else
newItem = new TextBlockSpan();
newItem.Text = textFragment;
newItem.Style = originalSpan.Style;
result.Add(newItem);
}
void AddParagraphSpacing()
{
if (ParagraphSpacing <= Size.Epsilon)
return;
// space ensure proper line spacing
result.Add(new TextBlockSpan() { Text = "\n ", Style = TextStyle.ParagraphSpacing });
result.Add(new TextBlockParagraphSpacing(0, ParagraphSpacing));
result.Add(new TextBlockSpan() { Text = " \n", Style = TextStyle.ParagraphSpacing });
}
void AddParagraphFirstLineIndentation()
{
if (ParagraphFirstLineIndentation <= Size.Epsilon)
return;
result.Add(new TextBlockSpan() { Text = "\n", Style = TextStyle.ParagraphSpacing });
result.Add(new TextBlockParagraphSpacing(ParagraphFirstLineIndentation, 0));
}
}
///
/// Adjusts the concurrency level for the SkParagraph.PlanLayout method to optimize performance.
///
/// While the Skia implementation is thread-safe, it appears to contain internal locks that hinder scalability.
/// Consequently, using multithreading for document rendering can reduce performance. This includes increased memory usage
/// and slower generation times—potentially even worse than rendering documents sequentially.
///
/// TODO: investigate further on how to improve scalability and remove this mutex
///
private static readonly object PlanLayoutLock = new();
private void CalculateParagraphMetrics(Size availableSpace)
{
if (Math.Abs(WidthForLineMetricsCalculation - availableSpace.Width) > Size.Epsilon)
AreParagraphMetricsValid = false;
if (AreParagraphMetricsValid)
return;
WidthForLineMetricsCalculation = availableSpace.Width;
lock (PlanLayoutLock)
Paragraph.PlanLayout(availableSpace.Width);
CheckUnresolvedGlyphs();
LineMetrics = Paragraph.GetLineMetrics();
PlaceholderPositions = Paragraph.GetPlaceholderPositions();
MaximumWidth = LineMetrics.Any() ? LineMetrics.Max(x => x.Width) : 0;
AreParagraphMetricsValid = true;
}
private void CheckUnresolvedGlyphs()
{
if (!Settings.CheckIfAllTextGlyphsAreAvailable)
return;
var unsupportedGlyphs = Paragraph.GetUnresolvedCodepoints();
if (!unsupportedGlyphs.Any())
return;
var formattedGlyphs = unsupportedGlyphs
.Select(codepoint =>
{
var character = char.ConvertFromUtf32(codepoint);
return $"U-{codepoint:X4} '{character}'";
});
var glyphs = string.Join("\n", formattedGlyphs);
throw new DocumentDrawingException(
$"Could not find an appropriate font fallback for the following glyphs: \n" +
$"${glyphs} \n\n" +
$"Possible solutions: \n" +
$"1) Install fonts that contain missing glyphs in your runtime environment. \n" +
$"2) Configure the fallback TextStyle using the 'TextStyle.FontFamilyFallback' method. \n" +
$"3) Register additional application specific fonts using the 'FontManager.RegisterFont' method. \n\n" +
$"You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. \n" +
$"However, this may result with text glyphs being incorrectly rendered without any warning.");
}
#region Handling Of Text Blocks With Only With Space
private static ConcurrentDictionary ParagraphContainingOnlyWhiteSpaceHeightCache { get; } = new(); // key: TextStyle.Id
private bool CheckIfContainsOnlyWhiteSpace()
{
foreach (var textBlockItem in Items)
{
// TextBlockPageNumber needs to be checked first, as it derives from TextBlockSpan,
// and before the generation starts, its Text property is empty
if (textBlockItem is TextBlockPageNumber)
return false;
if (textBlockItem is TextBlockSpan textBlockSpan && !string.IsNullOrWhiteSpace(textBlockSpan.Text))
return false;
if (textBlockItem is TextBlockElement)
return false;
}
return true;
}
private float MeasureHeightOfParagraphContainingOnlyWhiteSpace()
{
return Items
.OfType()
.Select(x => ParagraphContainingOnlyWhiteSpaceHeightCache.GetOrAdd(x.Style.Id, Measure))
.DefaultIfEmpty(0)
.Max();
static float Measure(int textStyleId)
{
var paragraphStyle = new ParagraphStyle
{
Alignment = ParagraphStyleConfiguration.TextAlign.Start,
Direction = ParagraphStyleConfiguration.TextDirection.Ltr,
MaxLinesVisible = 1_000_000,
LineClampEllipsis = string.Empty
};
var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
try
{
var textStyle = TextStyleManager.GetTextStyle(textStyleId).GetSkTextStyle();
builder.AddText("\u00A0", textStyle); // non-breaking space
using var paragraph = builder.CreateParagraph();
paragraph.PlanLayout(1000);
return paragraph.GetLineMetrics().First().Height;
}
finally
{
SkParagraphBuilderPoolManager.Return(builder);
}
}
}
#endregion
#region IStateful
private bool IsRendered { get; set; }
private int CurrentLineIndex { get; set; }
private float CurrentTopOffset { get; set; }
public struct TextBlockState
{
public bool IsRendered;
public int CurrentLineIndex;
public float CurrentTopOffset;
}
public void ResetState(bool hardReset = false)
{
IsRendered = false;
CurrentLineIndex = 0;
CurrentTopOffset = 0;
}
public object GetState()
{
return new TextBlockState
{
IsRendered = IsRendered,
CurrentLineIndex = CurrentLineIndex,
CurrentTopOffset = CurrentTopOffset
};
}
public void SetState(object state)
{
var textBlockState = (TextBlockState) state;
IsRendered = textBlockState.IsRendered;
CurrentLineIndex = textBlockState.CurrentLineIndex;
CurrentTopOffset = textBlockState.CurrentTopOffset;
}
#endregion
internal override string? GetCompanionHint() => Text.Substring(0, Math.Min(Text.Length, 50));
internal override string? GetCompanionSearchableContent() => Text;
}
}
================================================
FILE: Source/QuestPDF/Elements/Translate.cs
================================================
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Translate : ContainerElement
{
public float TranslateX { get; set; } = 0;
public float TranslateY { get; set; } = 0;
internal override void Draw(Size availableSpace)
{
var translate = new Position(TranslateX, TranslateY);
Canvas.Translate(translate);
base.Draw(availableSpace);
Canvas.Translate(translate.Reverse());
}
internal override string? GetCompanionHint()
{
return string.Join(" ", GetOptions().Where(x => x.value != 0).Select(x => $"{x.Label}={x.value.FormatAsCompanionNumber()}"));
IEnumerable<(string Label, float value)> GetOptions()
{
yield return ("X", TranslateX);
yield return ("Y", TranslateY);
}
}
}
}
================================================
FILE: Source/QuestPDF/Elements/Unconstrained.cs
================================================
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class Unconstrained : ContainerElement, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
var childSize = base.Measure(Size.Max);
if (childSize.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(0, 0);
if (childSize.Type == SpacePlanType.FullRender)
return SpacePlan.FullRender(0, 0);
return childSize;
}
internal override void Draw(Size availableSpace)
{
var measurement = base.Measure(Size.Max);
if (measurement.Type is SpacePlanType.Empty or SpacePlanType.Wrap)
return;
var translate = ContentDirection == ContentDirection.RightToLeft
? new Position(-measurement.Width, 0)
: Position.Zero;
Canvas.Translate(translate);
base.Draw(measurement);
Canvas.Translate(translate.Reverse());
}
}
}
================================================
FILE: Source/QuestPDF/Elements/ZIndex.cs
================================================
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal sealed class ZIndex : ContainerElement
{
public int Depth { get; set; }
internal override void Draw(Size availableSpace)
{
var previousZIndex = Canvas.GetZIndex();
Canvas.SetZIndex(Depth);
base.Draw(availableSpace);
Canvas.SetZIndex(previousZIndex);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/AlignmentExtensions.cs
================================================
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class AlignmentExtensions
{
#region Horizontal
private static IContainer AlignHorizontal(this IContainer element, HorizontalAlignment horizontalAlignment)
{
var alignment = element as Alignment ?? new Alignment();
alignment.Horizontal = horizontalAlignment;
return element.Element(alignment);
}
///
/// Aligns content horizontally to the left side.
/// Learn more
///
public static IContainer AlignLeft(this IContainer element)
{
return element.AlignHorizontal(HorizontalAlignment.Left);
}
///
/// Aligns content horizontally to the center, ensuring equal space on both left and right sides.
/// Learn more
///
public static IContainer AlignCenter(this IContainer element)
{
return element.AlignHorizontal(HorizontalAlignment.Center);
}
///
/// Aligns its content horizontally to the right side.
/// Learn more
///
public static IContainer AlignRight(this IContainer element)
{
return element.AlignHorizontal(HorizontalAlignment.Right);
}
#endregion
#region Vertical
private static IContainer AlignVertical(this IContainer element, VerticalAlignment verticalAlignment)
{
var alignment = element as Alignment ?? new Alignment();
alignment.Vertical = verticalAlignment;
return element.Element(alignment);
}
///
/// Aligns content vertically to the upper side.
/// Learn more
///
public static IContainer AlignTop(this IContainer element)
{
return element.AlignVertical(VerticalAlignment.Top);
}
///
/// Aligns content vertically to the center, ensuring equal space above and below.
/// Learn more
///
public static IContainer AlignMiddle(this IContainer element)
{
return element.AlignVertical(VerticalAlignment.Middle);
}
///
/// Aligns content vertically to the bottom side.
/// Learn more
///
public static IContainer AlignBottom(this IContainer element)
{
return element.AlignVertical(VerticalAlignment.Bottom);
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Fluent/ColumnExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class ColumnDescriptor
{
internal Column Column { get; } = new();
internal ColumnDescriptor()
{
}
///
/// Adjusts vertical spacing between items.
///
public void Spacing(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "The column spacing cannot be negative.");
Column.Spacing = value.ToPoints(unit);
}
///
/// Adds a new item to the column element.
///
/// The container of the newly created item.
public IContainer Item()
{
var container = new Container();
Column.Items.Add(container);
return container;
}
}
public static class ColumnExtensions
{
[Obsolete("This element has been renamed since version 2022.2. Please use the 'Column' method.")]
[ExcludeFromCodeCoverage]
public static void Stack(this IContainer element, Action handler)
{
element.Column(handler);
}
///
/// Draws a collection of elements vertically (from top to bottom).
/// Learn more
///
///
/// Supports paging.
///
/// The action to configure the column's content.
public static void Column(this IContainer element, Action handler)
{
var descriptor = new ColumnDescriptor();
handler(descriptor);
element.Element(descriptor.Column);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/ComponentExtentions.cs
================================================
using System;
using System.Linq.Expressions;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class ComponentExtensions
{
///
/// Instance of the class implementing the interface.
public static void Component(this IContainer element, T component) where T : IComponent
{
var componentContainer = element
.Container()
.DebugPointer(DebugPointerType.Component, component.GetType().Name);
component.Compose(componentContainer);
}
///
public static void Component(this IContainer element) where T : IComponent, new()
{
element.Component(new T());
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/ConstrainedExtensions.cs
================================================
using System;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class ConstrainedExtensions
{
#region Width
private static IContainer ConstrainedWidth(this IContainer element, float? min = null, float? max = null)
{
var constrained = element as Constrained ?? new Constrained();
if (min < 0)
throw new ArgumentOutOfRangeException(nameof(min), "The minimum width cannot be negative.");
if (max < 0)
throw new ArgumentOutOfRangeException(nameof(max), "The maximum width cannot be negative.");
if (min > max)
throw new ArgumentOutOfRangeException(nameof(min), "The minimum width cannot be greater than the maximum width.");
if (min.HasValue)
constrained.MinWidth = min;
if (max.HasValue)
constrained.MaxWidth = max;
return element.Element(constrained);
}
///
/// Sets the exact width of its content.
/// Learn more
///
/// The container with the specified exact width.
public static IContainer Width(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.ConstrainedWidth(min: value, max: value);
}
///
/// Sets the minimum width of its content.
/// Learn more
///
/// The container with the specified minimum width.
public static IContainer MinWidth(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.ConstrainedWidth(min: value);
}
///
/// Sets the maximum width of its content.
/// Learn more
///
/// The container with the specified maximum width.
public static IContainer MaxWidth(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.ConstrainedWidth(max: value);
}
#endregion
#region Height
private static IContainer ConstrainedHeight(this IContainer element, float? min = null, float? max = null)
{
var constrained = element as Constrained ?? new Constrained();
if (min < 0)
throw new ArgumentOutOfRangeException(nameof(min), "The minimum height cannot be negative.");
if (max < 0)
throw new ArgumentOutOfRangeException(nameof(max), "The maximum height cannot be negative.");
if (min > max)
throw new ArgumentOutOfRangeException(nameof(min), "The minimum height cannot be greater than the maximum height.");
if (min.HasValue)
constrained.MinHeight = min;
if (max.HasValue)
constrained.MaxHeight = max;
return element.Element(constrained);
}
///
/// Sets the exact height of its content.
/// Learn more
///
/// The container with the specified exact height.
public static IContainer Height(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.ConstrainedHeight(min: value, max: value);
}
///
/// Sets the minimum height of its content.
/// Learn more
///
/// The container with the specified minimum height.
public static IContainer MinHeight(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.ConstrainedHeight(min: value);
}
///
/// Sets the maximum height of its content.
/// Learn more
///
/// The container with the specified maximum height.
public static IContainer MaxHeight(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.ConstrainedHeight(max: value);
}
#endregion
internal static IContainer EnforceSizeWhenEmpty(this IContainer element)
{
(element as Constrained).EnforceSizeWhenEmpty = true;
return element;
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/ContentDirectionExtensions.cs
================================================
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class ContentDirectionExtensions
{
internal static IContainer ContentDirection(this IContainer element, ContentDirection direction)
{
return element.Element(new ContentDirectionSetter
{
ContentDirection = direction
});
}
///
/// Sets the left-to-right (LTR) direction for its entire content.
/// Learn more
///
///
public static IContainer ContentFromLeftToRight(this IContainer element)
{
return element.ContentDirection(Infrastructure.ContentDirection.LeftToRight);
}
///
/// Sets the right-to-left (RTL) direction for its entire content.
/// Learn more
///
///
public static IContainer ContentFromRightToLeft(this IContainer element)
{
return element.ContentDirection(Infrastructure.ContentDirection.RightToLeft);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/DebugExtensions.cs
================================================
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class DebugExtensions
{
///
/// Draws a labeled box around its inner content.
/// Useful for visual debugging and pinpointing output from specific code blocks.
///
/// Learn more
///
/// Optional label displayed within the box.
///
public static IContainer DebugArea(this IContainer parent, string? text = null, Color? color = null)
{
var container = new Container();
parent.Component(new DebugArea
{
Child = container,
Text = text ?? string.Empty,
Color = color ?? Colors.Red.Medium
});
return container;
}
///
///
/// Inserts a virtual debug element visible in the document hierarchy tree in the QuestPDF Companion App,
/// as well as in the enhanced debugging message provided by the .
///
///
/// It helps with understanding and navigation of the document hierarchy.
/// Learn more
///
///
/// This debug element does not appear in the final PDF output.
///
/// Text visible somewhere in the "element trace" content identifying given document fragment.
public static IContainer DebugPointer(this IContainer parent, string label)
{
return parent.DebugPointer(DebugPointerType.UserDefined, label);
}
internal static IContainer DebugPointer(this IContainer parent, DebugPointerType type, string label)
{
return parent.Element(new DebugPointer
{
Type = type,
Label = label
});
}
internal static IContainer LayoutOverflowVisualization(this IContainer parent)
{
return parent.Element(new LayoutOverflowVisualization());
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/DecorationExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class DecorationDescriptor
{
internal Decoration Decoration { get; } = new Decoration();
internal DecorationDescriptor()
{
}
///
/// Returns a container for the section positioned before (above) the primary main content.
///
///
/// This container is fully visible on each page and does not support paging.
///
public IContainer Before()
{
if (Decoration.Before is not (Empty or DebugPointer))
throw new DocumentComposeException("The 'Decoration.Before' layer has already been defined. Please call this method only once.");
var container = new Container();
Decoration.Before = container;
return container
.DebugPointer(DebugPointerType.ElementStructure, "Before")
.RepeatAsHeader();
}
///
/// Provides a handler to the section that appears before (above) the main content.
///
///
/// This container is fully visible on each page and does not support paging.
///
public void Before(Action handler)
{
handler?.Invoke(Before());
}
///
/// Returns a container for the main section.
///
///
/// This container does support paging.
///
public IContainer Content()
{
if (Decoration.Content is not (Empty or DebugPointer))
throw new DocumentComposeException("The 'Decoration.Content' layer has already been defined. Please call this method only once.");
var container = new Container();
Decoration.Content = container;
return container.DebugPointer(DebugPointerType.ElementStructure, "Content");
}
///
/// Provides a handler to define content of the main section.
///
///
/// This container does support paging.
///
public void Content(Action handler)
{
handler?.Invoke(Content());
}
///
/// Returns a container for the section positioned after (below) the main content.
///
///
/// This container is fully visible on each page and does not support paging.
///
public IContainer After()
{
if (Decoration.After is not (Empty or DebugPointer))
throw new DocumentComposeException("The 'Decoration.After' layer has already been defined. Please call this method only once.");
var container = new Container();
Decoration.After = container;
return container
.DebugPointer(DebugPointerType.ElementStructure, "After")
.RepeatAsFooter();
}
///
/// Provides a handler to the section that appears after (below) the main content.
///
///
/// This container is fully visible on each page and does not support paging.
///
public void After(Action handler)
{
handler?.Invoke(After());
}
#region Obsolete
[Obsolete("This element has been renamed since version 2022.2. Please use the 'Before' method.")]
[ExcludeFromCodeCoverage]
public IContainer Header()
{
var container = new Container();
Decoration.Before = container;
return container;
}
[Obsolete("This element has been renamed since version 2022.2. Please use the 'Before' method.")]
[ExcludeFromCodeCoverage]
public void Header(Action handler)
{
handler?.Invoke(Header());
}
[Obsolete("This element has been renamed since version 2022.2. Please use the 'After' method.")]
[ExcludeFromCodeCoverage]
public IContainer Footer()
{
var container = new Container();
Decoration.After = container;
return container;
}
[Obsolete("This element has been renamed since version 2022.2. Please use the 'After' method.")]
[ExcludeFromCodeCoverage]
public void Footer(Action handler)
{
handler?.Invoke(Footer());
}
#endregion
}
public static class DecorationExtensions
{
///
/// Divides the container's space into three distinct sections: before, content, and after.
/// The "before" section is rendered above the main content, while the "after" section is rendered below it.
/// If the main "content" spans across multiple pages, both the "before" and "after" sections are consistently rendered on every page.
///
/// Learn more
///
///
/// A typical use-case for this method is to render a table that spans multiple pages, with a consistent caption or header on each page.
///
/// The action to configure the content.
public static void Decoration(this IContainer element, Action handler)
{
var descriptor = new DecorationDescriptor();
handler(descriptor);
element.Element(descriptor.Decoration);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/DocumentOperation.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using QuestPDF.Qpdf;
namespace QuestPDF.Fluent;
///
/// Provides functionality for performing various operations on PDF documents, including loading, merging, overlaying, underlaying, selecting specific pages, adding attachments, and applying encryption settings.
///
public sealed class DocumentOperation
{
///
/// Represents configuration options for applying an overlay or underlay to a PDF document using qpdf.
///
public sealed class LayerConfiguration
{
///
/// The file path of the overlay or underlay PDF file to be used.
///
public string FilePath { get; set; }
///
/// Specifies the range of pages in the output document where the overlay or underlay will be applied.
/// If not specified, the overlay or underlay is applied to all output pages.
///
///
public string? TargetPages { get; set; }
///
/// Specifies the range of pages in the overlay or underlay file to be used initially.
/// If not specified, all pages in the overlay or underlay file will be used in sequence.
///
///
public string? SourcePages { get; set; }
///
/// Specifies an optional range of pages in the overlay or underlay file that will repeat after the initial source pages are exhausted.
/// Useful for repeating certain pages of the overlay or underlay file across multiple pages of the output.
///
///
public string? RepeatSourcePages { get; set; }
}
public enum DocumentAttachmentRelationship
{
///
/// Indicates data files relevant to the document (e.g., supporting datasets or data tables).
///
Data,
///
/// Represents a source file directly used to create the document.
///
Source,
///
/// An alternative representation of the document content (e.g., XML, HTML).
///
Alternative,
///
/// A file supplementing the content, like additional resources.
///
Supplement,
///
/// No specific relationship is defined.
///
Unspecified
}
public sealed class DocumentAttachment
{
///
/// Sets the key for the attachment, specific to the PDF format.
/// Defaults to the file name without its path.
///
public string? Key { get; set; }
///
/// The file path of the attachment. Ensure that the specified file exists.
///
public string FilePath { get; set; }
///
/// Specifies the display name for the attachment.
/// This name is typically shown to the user and used by most graphical PDF viewers when saving the file.
/// Defaults to the file name without its path.
///
public string? AttachmentName { get; set; }
///
/// Specifies the creation date of the attachment.
/// Defaults to the file's creation time.
///
public DateTime? CreationDate { get; set; }
///
/// Specifies the modification date of the attachment.
/// Defaults to the file's last modified time.
///
public DateTime? ModificationDate { get; set; }
///
/// Specifies the MIME type of the attachment, such as "text/plain", "application/pdf", "image/png", etc.
///
public string? MimeType { get; set; }
///
/// Sets a description for the attachment, which may be displayed by some PDF viewers.
///
public string? Description { get; set; }
///
/// Indicates whether to replace an existing attachment with the same key.
/// If false, an exception is thrown if an attachment with the same key already exists.
///
public bool Replace { get; set; } = true;
///
/// Specifies the relationship of the embedded file to the document for PDF/A-3b compliance.
///
public DocumentAttachmentRelationship? Relationship { get; set; } = null;
}
public class EncryptionBase
{
///
/// The user password for the PDF, allowing restricted access based on encryption settings.
/// May be left null to enable opening the PDF without a password, though this may restrict certain operations.
///
public string? UserPassword { get; set; }
///
/// The owner password for the PDF, granting full access to all document features.
/// An empty owner password is considered insecure, as is using the same value for both user and owner passwords.
///
public string OwnerPassword { get; set; }
}
public sealed class Encryption40Bit : EncryptionBase
{
///
public bool AllowAnnotation { get; set; } = true;
///
public bool AllowContentExtraction { get; set; } = true;
///
public bool AllowModification { get; set; } = true;
///
public bool AllowPrinting { get; set; } = true;
}
public sealed class Encryption128Bit : EncryptionBase
{
///
public bool AllowAnnotation { get; set; } = true;
///
public bool AllowAssembly { get; set; } = true;
///
public bool AllowContentExtraction { get; set; } = true;
///
public bool AllowFillingForms { get; set; } = true;
///
public bool AllowPrinting { get; set; } = true;
///
public bool EncryptMetadata { get; set; } = true;
}
public sealed class Encryption256Bit : EncryptionBase
{
///
public bool AllowAnnotation { get; set; } = true;
///
public bool AllowAssembly { get; set; } = true;
///
public bool AllowContentExtraction { get; set; } = true;
///
public bool AllowFillingForms { get; set; } = true;
///
public bool AllowPrinting { get; set; } = true;
///
public bool EncryptMetadata { get; set; } = true;
}
internal JobConfiguration Configuration { get; private set; }
private DocumentOperation()
{
}
///
/// Loads the specified PDF file for processing, enabling operations such as merging, overlaying or underlaying content, selecting pages, adding attachments, and encrypting.
///
/// The full path to the PDF file to be loaded.
/// The password for the PDF file, if it is password-protected. Optional.
public static DocumentOperation LoadFile(string filePath, string? password = null)
{
if (!File.Exists(filePath))
throw new Exception($"The file could not be found: {filePath}");
return new DocumentOperation
{
Configuration = new JobConfiguration
{
InputFile = filePath,
Password = password
}
};
}
///
/// Selects specific pages from the current document based on the provided page selector, marking them for further operations.
///
///
public DocumentOperation TakePages(string pageSelector)
{
Configuration.Pages ??= new List();
Configuration.Pages.Add(new JobConfiguration.PageConfiguration
{
File = ".",
Range = pageSelector
});
return this;
}
///
/// Merges pages from the specified PDF file into the current document, according to the provided page selection.
///
/// The path to the PDF file to be merged.
/// An optional to specify the range of pages to merge. If not provided, all pages will be merged.
///
public DocumentOperation MergeFile(string filePath, string? pageSelector = null)
{
if (!File.Exists(filePath))
throw new Exception($"The file could not be found: {filePath}");
if (Configuration.Pages == null)
TakePages("1-z");
Configuration.Pages.Add(new JobConfiguration.PageConfiguration
{
File = filePath,
Range = pageSelector ?? "1-z"
});
return this;
}
///
/// Applies an underlay to the document using the specified configuration.
/// The underlay pages are drawn beneath the target pages in the output file, potentially obscured by the original content.
///
public DocumentOperation UnderlayFile(LayerConfiguration configuration)
{
if (!File.Exists(configuration.FilePath))
throw new Exception($"The file could not be found: {configuration.FilePath}");
Configuration.Underlay ??= new List();
Configuration.Underlay.Add(new JobConfiguration.LayerConfiguration
{
File = configuration.FilePath,
To = configuration.TargetPages,
From = configuration.SourcePages,
Repeat = configuration.RepeatSourcePages
});
return this;
}
///
/// Applies an overlay to the document using the specified configuration.
/// The overlay pages are drawn on top of the target pages in the output file, potentially obscuring the original content.
///
public DocumentOperation OverlayFile(LayerConfiguration configuration)
{
if (!File.Exists(configuration.FilePath))
throw new Exception($"The file could not be found: {configuration.FilePath}");
Configuration.Overlay ??= new List();
Configuration.Overlay.Add(new JobConfiguration.LayerConfiguration
{
File = configuration.FilePath,
To = configuration.TargetPages,
From = configuration.SourcePages,
Repeat = configuration.RepeatSourcePages
});
return this;
}
///
/// Extends the current document's XMP metadata by adding content within the rdf:Description tag.
/// This allows for adding additional descriptive metadata to the PDF, which is useful for compliance standards
/// like PDF/A or for industry-specific metadata (e.g., ZUGFeRD).
///
///
/// A string containing the metadata to add. This metadata must be valid XML content and conform to the
/// RDF structure required by the PDF XMP metadata specification.
///
public DocumentOperation ExtendMetadata(string metadata)
{
Configuration.ExtendMetadata = metadata;
return this;
}
///
/// Adds an attachment to the document, with specified metadata and configuration options.
///
public DocumentOperation AddAttachment(DocumentAttachment attachment)
{
Configuration.AddAttachment ??= new List();
if (!File.Exists(attachment.FilePath))
throw new Exception($"The file could not be found: {attachment.FilePath}");
var file = new FileInfo(attachment.FilePath);
Configuration.AddAttachment.Add(new JobConfiguration.AddDocumentAttachment
{
Key = attachment.Key ?? Path.GetFileName(attachment.FilePath),
File = attachment.FilePath,
FileName = attachment.AttachmentName ?? file.Name,
CreationDate = GetFormattedDate(attachment.CreationDate, File.GetCreationTimeUtc(attachment.FilePath)),
ModificationDate = GetFormattedDate(attachment.ModificationDate, File.GetLastWriteTime(attachment.FilePath)),
MimeType = attachment.MimeType ?? GetDefaultMimeType(),
Description = attachment.Description,
Replace = attachment.Replace ? string.Empty : null,
Relationship = GetRelationship(attachment.Relationship)
});
return this;
string GetDefaultMimeType()
{
var fileExtension = Path.GetExtension(attachment.FilePath);
fileExtension = fileExtension.TrimStart('.').ToLowerInvariant();
return MimeHelper.FileExtensionToMimeConversionTable.TryGetValue(fileExtension, out var value) ? value : "text/plain";
}
string GetFormattedDate(DateTime? value, DateTime defaultValue)
{
return $"D:{(value ?? defaultValue).ToUniversalTime():yyyyMMddHHmmsss}Z";
}
string? GetRelationship(DocumentAttachmentRelationship? relationship)
{
return relationship switch
{
DocumentAttachmentRelationship.Data => "/Data",
DocumentAttachmentRelationship.Source => "/Source",
DocumentAttachmentRelationship.Alternative => "/Alternative",
DocumentAttachmentRelationship.Supplement => "/Alternative",
DocumentAttachmentRelationship.Unspecified => "/Unspecified",
null => null,
_ => throw new ArgumentOutOfRangeException(nameof(relationship), relationship, null)
};
}
}
///
/// Removes any existing encryption from the current PDF document, effectively making it accessible without a password or encryption restrictions.
///
public DocumentOperation Decrypt()
{
Configuration.Decrypt = string.Empty;
return this;
}
///
/// Remove security restrictions associated with digitally signed PDF files.
/// This may be combined with Decrypt() operation to allow free editing of previously signed/encrypted files.
/// This option invalidates and disables any digital signatures but leaves their visual appearances intact.
///
public DocumentOperation RemoveRestrictions()
{
Configuration.Decrypt = string.Empty;
Configuration.RemoveRestrictions = string.Empty;
return this;
}
///
/// Encrypts the document using 40-bit encryption, applying specified owner and user passwords along with defined permissions.
///
public DocumentOperation Encrypt(Encryption40Bit encryption)
{
if (Configuration.Encrypt != null)
throw new InvalidOperationException("Encryption process can be set only once");
Configuration.Encrypt = new JobConfiguration.EncryptionSettings
{
UserPassword = encryption.UserPassword,
OwnerPassword = encryption.OwnerPassword,
Options40Bit = new JobConfiguration.Encryption40Bit
{
Annotate = FormatBooleanFlag(encryption.AllowAnnotation),
Extract = FormatBooleanFlag(encryption.AllowContentExtraction),
Modify = encryption.AllowModification ? "all" : "none",
Print = encryption.AllowPrinting ? "full" : "none",
}
};
return this;
}
///
/// Encrypts the document using 128-bit encryption, applying specified owner and user passwords along with defined permissions.
///
public DocumentOperation Encrypt(Encryption128Bit encryption)
{
if (Configuration.Encrypt != null)
throw new InvalidOperationException("Encryption process can be set only once");
Configuration.Encrypt = new JobConfiguration.EncryptionSettings
{
UserPassword = encryption.UserPassword,
OwnerPassword = encryption.OwnerPassword,
Options128Bit = new JobConfiguration.Encryption128Bit
{
Annotate = FormatBooleanFlag(encryption.AllowAnnotation),
Assemble = FormatBooleanFlag(encryption.AllowAssembly),
Extract = FormatBooleanFlag(encryption.AllowContentExtraction),
Form = FormatBooleanFlag(encryption.AllowFillingForms),
Print = encryption.AllowPrinting ? "full" : "none",
CleartextMetadata = encryption.EncryptMetadata ? null : string.Empty
}
};
return this;
}
///
/// Encrypts the document using 256-bit encryption, applying specified owner and user passwords along with defined permissions.
///
public DocumentOperation Encrypt(Encryption256Bit encryption)
{
if (Configuration.Encrypt != null)
throw new InvalidOperationException("Encryption process can be set only once");
Configuration.Encrypt = new JobConfiguration.EncryptionSettings
{
UserPassword = encryption.UserPassword,
OwnerPassword = encryption.OwnerPassword,
Options256Bit = new JobConfiguration.Encryption256Bit
{
Annotate = FormatBooleanFlag(encryption.AllowAnnotation),
Assemble = FormatBooleanFlag(encryption.AllowAssembly),
Extract = FormatBooleanFlag(encryption.AllowContentExtraction),
Form = FormatBooleanFlag(encryption.AllowFillingForms),
Print = encryption.AllowPrinting ? "full" : "none",
CleartextMetadata = encryption.EncryptMetadata ? null : string.Empty
}
};
return this;
}
private string FormatBooleanFlag(bool value)
{
return value ? "y" : "n";
}
///
/// Creates linearized (web-optimized) output files.
/// Linearized files are structured to allow compliant PDF readers to begin displaying content before the entire file is downloaded.
/// Normally, a PDF reader requires the entire file to be present to render content, as essential cross-reference data typically appears at the file’s end.
///
public DocumentOperation Linearize()
{
Configuration.Linearize = string.Empty;
return this;
}
///
/// Executes the configured operations on the document and saves the resulting file to the specified path.
///
/// The path where the output file will be saved.
public void Save(string filePath)
{
if (File.Exists(filePath))
File.Delete(filePath);
Configuration.OutputFile = filePath;
var json = SimpleJsonSerializer.Serialize(Configuration);
QpdfAPI.ExecuteJob(json);
}
}
================================================
FILE: Source/QuestPDF/Fluent/DynamicComponentExtensions.cs
================================================
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class DynamicComponentExtensions
{
///
/// Represents a dynamically generated section of the document.
/// Components are page-aware, understand their positioning, can dynamically construct other content elements, and assess their dimensions, enabling complex layout creations.
/// Learn more
///
///
///
/// Consider an invoice that presents all purchased items in a table format.
/// Instead of just showing the final total price under the table, the requirement is to display the cumulative prices on each separate page.
///
/// Using the dynamic component, you can manually assemble the table, count how many items are visible on each page, calculate the price sum for items visible on each page, and then render the result under each sub-table.
///
public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement)
{
var componentProxy = DynamicComponentProxy.CreateFrom(dynamicElement);
element.Element(new DynamicHost(componentProxy));
}
///
/// Represents a section of the document dynamically created based on its inner state.
/// Components are page-aware, understand their positioning, can dynamically construct other content elements, and assess their dimensions, enabling complex layout creations.
/// Learn more
///
///
///
/// Consider an invoice that presents all purchased items in a table format.
/// Instead of just showing the final total price under the table, the requirement is to display the cumulative prices on each separate page.
///
/// Using the dynamic component, you can manually assemble the table, count how many items are visible on each page, calculate the price sum for items visible on each page, and then render the result under each sub-table.
///
public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement) where TState : struct
{
var componentProxy = DynamicComponentProxy.CreateFrom(dynamicElement);
element.DebugPointer(DebugPointerType.Dynamic, dynamicElement.GetType().Name).Element(new DynamicHost(componentProxy));
}
///
/// Allows to inject the unattached content created by the method within the Dynamic component.
///
public static void Element(this IContainer element, IDynamicElement child)
{
ElementExtensions.Element(element, child);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/ElementExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Mime;
using System.Runtime.CompilerServices;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Fluent
{
///
/// Provides extension methods for manipulating and enhancing elements within a container.
///
public static class ElementExtensions
{
static ElementExtensions()
{
SkNativeDependencyCompatibilityChecker.Test();
}
internal static T Element(this IContainer element, T child) where T : IElement
{
if (element?.Child != null && element.Child is Empty == false)
{
var message = "You should not assign multiple child elements to a single-child container. " +
"This may happen when a container variable is used outside of its scope/closure OR the container is used in multiple fluent API chains OR the container is used incorrectly in a loop. " +
"This exception is thrown to help you detect that some part of the code is overriding fragments of the document layout with a new content - essentially destroying existing content.";
throw new DocumentComposeException(message);
}
if (element != child as Element)
element.Child = child as Element;
if (child is Element childElement)
childElement.CodeLocation = SourceCodePath.CreateFromCurrentStackTrace();
return child;
}
///
/// Passes the Fluent API chain to the provided method.
/// Learn more
///
///
/// This method is particularly useful for code refactoring, improving its structure and readability.
/// Extracting implementation of certain layout structures into separate methods, allows you to accurately describe their purpose and reuse them code in various parts of the application.
///
/// A delegate that takes the current container and populates it with content.
public static void Element(
this IContainer parent,
Action handler,
[CallerArgumentExpression("handler")] string handlerName = null,
[CallerMemberName] string parentName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var handlerContainer = parent
.Container()
.Element(new SourceCodePointer
{
MethodName = handlerName,
CalledFrom = parentName,
FilePath = sourceFilePath,
LineNumber = sourceLineNumber
});
handler(handlerContainer);
}
///
/// Passes the Fluent API chain to the provided method.
/// Learn more
///
///
/// This method is particularly useful for code refactoring, improving its structure and readability.
/// Extracting implementation of certain layout structures into separate methods, allows you to accurately describe their purpose and reuse them code in various parts of the application.
///
/// A method that accepts the current container, optionally populates it with content, and returns a subsequent container to continue the Fluent API chain.
/// The container returned by the method.
public static IContainer Element(
this IContainer parent,
Func handler,
[CallerArgumentExpression("handler")] string handlerName = null,
[CallerMemberName] string parentName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var handlerContainer = parent
.Element(new SourceCodePointer
{
MethodName = handlerName,
CalledFrom = parentName,
FilePath = sourceFilePath,
LineNumber = sourceLineNumber
});
return handler(handlerContainer);
}
public static void Element(this IContainer parent, IContainer child)
{
parent.Child = child as IElement;
}
internal static IContainer NonTrackingElement(this IContainer parent, Func handler)
{
return handler(parent.Container());
}
///
/// Constrains its content to maintain a given width-to-height ratio.
/// Learn more
///
///
/// This container enforces strict space constraints. The may be thrown if these constraints can't be satisfied.
///
/// Represents the aspect ratio as a width-to-height division. For instance, a container with a width of 250 points and a height of 200 points has an aspect ratio of 1.25.
/// Determines the approach the component should adopt when maintaining the specified aspect ratio.
public static IContainer AspectRatio(this IContainer element, float ratio, AspectRatioOption option = AspectRatioOption.FitWidth)
{
if (ratio <= 0)
throw new ArgumentOutOfRangeException(nameof(ratio), "The aspect ratio must be greater than zero.");
return element.Element(new AspectRatio
{
Ratio = ratio,
Option = option
});
}
///
/// Draws a basic placeholder useful for prototyping.
/// Learn more
///
///
/// You can control the size of the Placeholder by chaining other elements before its invocation, e.g.:
///
/// .Width(200)
/// .Height(100)
/// .Placeholder("Sample text");
///
///
/// When provided, the placeholder displays this text. If omitted, a simple image icon is shown instead.
public static void Placeholder(this IContainer element, string? text = null)
{
element.Component(new Placeholder
{
Text = text ?? string.Empty
});
}
///
/// The ShowOnce element modifies how content is displayed across multiple pages.
///
/// By default, all elements are fully rendered only once and never repeated.
/// However, in some contexts such as page headers and footers, as well as decoration before and after slots, the content is repeated on every page.
/// To prevent this, you can use the ShowOnce element.
///
/// Learn more
///
///
/// Combine this element with SkipOnce to achieve more complex behaviors, e.g.:
/// .SkipOnce().ShowOnce() ensures the child element is displayed only on the second page.
/// .SkipOnce().SkipOnce() starts displaying the child element from the third page onwards.
/// .ShowOnce().SkipOnce() draws nothing, as the order of invocation is important.
///
public static IContainer ShowOnce(this IContainer element)
{
return element.Element(new ShowOnce());
}
///
/// If the container spans multiple pages, its content is omitted on the first page and then displayed on the second and subsequent pages.
/// Learn more
///
///
/// A common use-case for this element is when displaying a consistent header across pages but needing to conditionally show/hide specific fragments on the first page.
///
///
/// Combine this element with ShowOnce to achieve more complex behaviors, e.g.:
/// .SkipOnce().ShowOnce() ensures the child element is displayed only on the second page.
/// .SkipOnce().SkipOnce() starts displaying the child element from the third page onwards.
/// .ShowOnce().SkipOnce() draws nothing, as the order of invocation is important.
///
public static IContainer SkipOnce(this IContainer element)
{
return element.Element(new SkipOnce());
}
///
/// Ensures its content is displayed entirely on a single page by disabling the default paging capability.
/// Learn more
///
///
/// While many library elements inherently support paging, allowing content to span multiple pages, this element restricts that behavior.
/// Employ this when a single-page display is crucial.
/// Be cautious: its strict space constraints can trigger the if content exceeds the page's capacity.
///
public static IContainer ShowEntire(this IContainer element)
{
return element.Element(new ShowEntire());
}
///
///
/// Ensures that the container's content occupies at least a specified minimum height on its first page of occurrence.
/// If there is enough space, the content is rendered as usual. However, if a page break is required,
/// this method ensures that a minimum amount of space is available before rendering the content.
/// If the required space is not available, the content is moved to the next page.
///
///
/// This rule applies only to the first page where the content appears. If the content spans multiple pages,
/// all subsequent pages are rendered without this restriction.
///
///
/// This method is particularly useful for structured elements like tables, where rendering only a small fragment
/// at the bottom of a page could negatively impact readability. By ensuring a minimum height,
/// you can prevent undesired content fragmentation.
///
///
/// Learn more
///
/// The minimum height, in points, that the element should occupy before a page break.
public static IContainer EnsureSpace(this IContainer element, float minHeight = Elements.EnsureSpace.DefaultMinHeight)
{
if (minHeight < 0)
throw new ArgumentOutOfRangeException(nameof(minHeight), "The EnsureSpace minimum height cannot be negative.");
return element.Element(new EnsureSpace
{
MinHeight = minHeight
});
}
///
///
/// Attempts to keep the container's content together on its first page of occurrence.
/// If the content does not fit entirely on that page, it is moved to the next page.
/// If it spans multiple pages, all subsequent pages are rendered as usual without restriction.
///
///
/// This method is useful for ensuring that content remains visually coherent and is not arbitrarily split.
///
///
/// Learn more
///
public static IContainer PreventPageBreak(this IContainer element)
{
return element.Element(new PreventPageBreak());
}
///
/// Inserts a break that pushes the subsequent content to start on a new page.
/// Learn more
///
public static void PageBreak(this IContainer element)
{
element.Element(new PageBreak());
}
///
/// A neutral layout structure that neither contributes to nor alters its content.
///
///
/// By default, certain FluentAPI calls may be batched together for optimized performance. Introduce this element if you wish to separate and prevent such optimizations.
///
public static IContainer Container(this IContainer element)
{
return element.Element(new Container());
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
[ExcludeFromCodeCoverage]
public static IContainer ExternalLink(this IContainer element, string url)
{
return element.Hyperlink(url);
}
///
/// Creates a clickable area that redirects the user to a designated webpage.
/// Learn more
///
///
public static IContainer Hyperlink(this IContainer element, string url)
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("The URL cannot be null or whitespace.", nameof(url));
return element.Element(new Hyperlink
{
Url = url
});
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Section method.")]
[ExcludeFromCodeCoverage]
public static IContainer Location(this IContainer element, string locationName)
{
return element.Section(locationName);
}
///
/// Defines a named fragment of the document that can span multiple pages.
/// Learn more
///
///
/// Several other elements interact with sections:
/// Use SectionLink to create a clickable area redirecting user to the first page of the associated section.
/// The Text element can display section properties, such as the starting page, ending page, and length.
///
/// An internal text key representing the section. It should be unique and won't appear in the final document.
public static IContainer Section(this IContainer element, string sectionName)
{
if (string.IsNullOrWhiteSpace(sectionName))
throw new ArgumentException("The section name cannot be null or whitespace.", nameof(sectionName));
return element
.DebugPointer(DebugPointerType.Section, sectionName)
.Element(new Section
{
SectionName = sectionName
});
}
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
[ExcludeFromCodeCoverage]
public static IContainer InternalLink(this IContainer element, string locationName)
{
return element.SectionLink(locationName);
}
///
/// Creates a clickable area that navigates the user to a designated section.
/// Learn more
///
///
public static IContainer SectionLink(this IContainer element, string sectionName)
{
if (string.IsNullOrWhiteSpace(sectionName))
throw new ArgumentException("The section name cannot be null or whitespace.", nameof(sectionName));
return element.Element(new SectionLink
{
SectionName = sectionName
});
}
///
/// Conditionally draws or hides its inner content.
/// Learn more
///
/// If the value is , its content is visible. Otherwise, it's hidden.
public static IContainer ShowIf(this IContainer element, bool condition)
{
return condition ? element : new Container();
}
///
/// Conditionally draws or hides its inner content depending on drawing context.
/// Please use carefully as certain predicates may produce unstable layouts resulting with unexpected content or exceptions.
/// Learn more
///
/// If the predicate returns , its content is visible. Otherwise, it's hidden.
public static IContainer ShowIf(this IContainer element, Predicate predicate)
{
return element.Element(new ShowIf
{
VisibilityPredicate = predicate
});
}
///
/// Removes size constraints and grants its content virtually unlimited space.
/// Learn more
///
public static IContainer Unconstrained(this IContainer element)
{
return element.Element(new Unconstrained());
}
///
/// Applies a default text style to all nested Text elements.
/// Learn more
///
///
/// If multiple text elements have a similar style, using this element can help simplify your code.
///
/// A TextStyle object used to override specific properties.
public static IContainer DefaultTextStyle(this IContainer element, TextStyle textStyle)
{
return element.Element(new DefaultTextStyle
{
TextStyle = textStyle
});
}
///
/// Applies a default text style to all nested Text elements.
/// Learn more
///
///
/// If multiple text elements have a similar style, using this element can help simplify your code.
///
/// A handler to modify the default text style.
public static IContainer DefaultTextStyle(this IContainer element, Func handler)
{
return element.Element(new DefaultTextStyle
{
TextStyle = handler(TextStyle.Default)
});
}
///
/// Renders the element exclusively on the first page. Any portion of the element that doesn't fit is omitted.
/// Learn more
///
public static IContainer StopPaging(this IContainer element)
{
return element.Element(new StopPaging());
}
///
/// Adjusts its content to fit within the available space by scaling it down proportionally if necessary.
/// Learn more
///
///
/// This container determines the best scale value through multiple scaling operations. With complex content, this may impact performance.
/// Pairing with certain elements, such as AspectRatio, might still lead to a .
///
public static IContainer ScaleToFit(this IContainer element)
{
return element.Element(new ScaleToFit());
}
///
/// Repeats its content across multiple pages.
/// Learn more
///
///
/// In certain layout structures, the content visibility may depend on other elements.
/// By default, most elements are rendered only once.
/// Use this element to repeat the content across multiple pages.
///
public static IContainer Repeat(this IContainer element)
{
return element.Element(new RepeatContent());
}
internal static IContainer RepeatAsHeader(this IContainer element)
{
return element.Element(new RepeatContent
{
RepeatContext = RepeatContent.RepeatContextType.PageHeader
});
}
internal static IContainer RepeatAsFooter(this IContainer element)
{
return element.Element(new RepeatContent
{
RepeatContext = RepeatContent.RepeatContextType.PageFooter
});
}
///
///
/// Delays the creation of document content and reduces its lifetime, significantly lowering memory usage in large documents containing thousands of pages.
/// This approach also enhances garbage collection efficiency in memory-constrained environments.
///
///
/// The provided delegate is invoked later in the document generation process.
/// For optimal performance, divide your document into smaller sections, each encapsulated within its own Lazy element.
/// Further optimizations can be achieved by nesting Lazy elements within each other.
/// However, note that this technique may increase the overall document generation time due to deferred content processing.
///
/// Learn more
///
public static void Lazy(this IContainer element, Action contentBuilder)
{
element.Element(new Lazy
{
ContentSource = contentBuilder,
IsCacheable = false
});
}
///
/// Functions similarly to the Lazy element but enables the library to use caching mechanisms for the content.
/// This can help optimize managed memory usage, although native memory usage may remain high.
/// Use LazyWithCache only when the increased generation time associated with the Lazy element is unacceptable.
/// Learn more
///
public static void LazyWithCache(this IContainer element, Action contentBuilder)
{
element.Element(new Lazy
{
ContentSource = contentBuilder,
IsCacheable = true
});
}
///
/// By default, the library draws content in the order it is defined, which may not always be the desired behavior.
/// This element allows you to alter the rendering order, ensuring that the content is displayed in the correct sequence.
/// The default z-index is 0, unless a different value is inherited from a parent container.
/// Learn more
///
/// The z-index value. Higher values are rendered above lower values.
public static IContainer ZIndex(this IContainer element, int indexValue)
{
return element.Element(new ZIndex
{
Depth = indexValue
});
}
///
/// Observes the rendering process of its content and captures its position and size on each page.
/// The captured data can be then used in the Dynamic component to build and position other elements.
/// Learn more
///
public static IContainer CaptureContentPosition(this IContainer element, string id)
{
return element.Element(new ElementPositionLocator
{
Id = id
});
}
#region Canvas [Obsolete]
private const string CanvasDeprecatedMessage = "The Canvas API has been deprecated since version 2024.3.0. Please use the .Svg(stringContent) API to provide custom content, and consult documentation webpage regarding integrating SkiaSharp with QuestPDF: https://www.questpdf.com/api-reference/skiasharp-integration.html";
[Obsolete(CanvasDeprecatedMessage)]
public delegate void DrawOnCanvas(object canvas, Size availableSpace);
[Obsolete(CanvasDeprecatedMessage)]
[ExcludeFromCodeCoverage]
public static void Canvas(this IContainer element, DrawOnCanvas handler)
{
throw new NotImplementedException(CanvasDeprecatedMessage);
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Fluent/ExtendExtensions.cs
================================================
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class ExtendExtensions
{
private static IContainer Extend(this IContainer element, bool vertical = false, bool horizontal = false)
{
var extend = element as Extend ?? new Extend();
extend.ExtendVertical |= vertical;
extend.ExtendHorizontal |= horizontal;
return element.Element(extend);
}
///
/// Forces its content to occupy entire available space, maximizing both width and height.
/// Learn more
///
public static IContainer Extend(this IContainer element)
{
return element.Extend(horizontal: true, vertical: true);
}
///
/// Forces its content to occupy entire available vertical space, maximizing height usage.
/// Learn more
///
public static IContainer ExtendVertical(this IContainer element)
{
return element.Extend(vertical: true);
}
///
/// Expands its content to occupy entire available horizontal space, maximizing width usage.
/// Learn more
///
public static IContainer ExtendHorizontal(this IContainer element)
{
return element.Extend(horizontal: true);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/GenerateExtensions.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Fluent
{
public static class GenerateExtensions
{
static GenerateExtensions()
{
ClearGenerateAndShowFiles();
}
internal static void Generate(this IDocument document, IDocumentCanvas documentCanvas)
{
DocumentGenerator.RenderDocument(documentCanvas, document, DocumentSettings.Default);
}
#region Genearate And Show Configuration
private static readonly Random Random = new();
private const string GenerateAndShowNamePrefix = "QuestPDF Preview";
private static void ClearGenerateAndShowFiles()
{
var legacyPreviewFiles = Directory
.GetFiles(TemporaryStorage.GetPath(), $"{GenerateAndShowNamePrefix} *")
.Where(x => DateTime.UtcNow - new FileInfo(x).LastAccessTimeUtc > TimeSpan.FromHours(1));
foreach (var legacyPreviewFile in legacyPreviewFiles)
{
try
{
if (File.Exists(legacyPreviewFile))
File.Delete(legacyPreviewFile);
}
catch
{
// ignored
}
}
}
#endregion
#region PDF
///
/// Generates the document in PDF format and returns it as a byte array.
///
public static byte[] GeneratePdf(this IDocument document)
{
using var memoryStream = new MemoryStream();
document.GeneratePdf(memoryStream);
return memoryStream.ToArray();
}
///
/// Generates the document in PDF format and saves it to the specified file path.
///
public static void GeneratePdf(this IDocument document, string filePath)
{
if (File.Exists(filePath))
File.Delete(filePath);
using var fileStream = File.Create(filePath);
document.GeneratePdf(fileStream);
}
///
/// Generates the document in PDF format and outputs it to a provided stream.
///
public static void GeneratePdf(this IDocument document, Stream stream)
{
using var skiaStream = new SkWriteStream(stream);
DocumentGenerator.GeneratePdf(skiaStream, document);
skiaStream.Flush();
}
///
/// Generates the document in PDF format, saves it in temporary file, and then opens it with the default application.
///
public static void GeneratePdfAndShow(this IDocument document)
{
var filePath = Path.Combine(TemporaryStorage.GetPath(), $"{GenerateAndShowNamePrefix} {Random.Next()}.pdf");
document.GeneratePdf(filePath);
Helpers.Helpers.OpenFileUsingDefaultProgram(filePath);
}
#endregion
#region XPS
///
/// Generates the document in XPS format and returns it as a byte array.
///
///
/// Supported only on the Windows platform.
///
public static byte[] GenerateXps(this IDocument document)
{
using var memoryStream = new MemoryStream();
document.GenerateXps(memoryStream);
return memoryStream.ToArray();
}
///
/// Generates the document in XPS format and saves it to the specified file path.
///
///
/// Supported only on the Windows platform.
///
public static void GenerateXps(this IDocument document, string filePath)
{
if (File.Exists(filePath))
File.Delete(filePath);
using var fileStream = File.Create(filePath);
document.GenerateXps(fileStream);
}
///
/// Generates the document in XPS format and outputs it to a provided stream.
///
///
/// Supported only on the Windows platform.
///
public static void GenerateXps(this IDocument document, Stream stream)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException("XPS generation is only supported on the Windows platform.");
using var skiaStream = new SkWriteStream(stream);
DocumentGenerator.GenerateXps(skiaStream, document);
skiaStream.Flush();
}
///
/// Generates the document in XPS format, saves it in temporary file, and then opens it with the default application.
///
///
/// Supported only on the Windows platform.
///
public static void GenerateXpsAndShow(this IDocument document)
{
var filePath = Path.Combine(TemporaryStorage.GetPath(), $"{GenerateAndShowNamePrefix} {Random.Next()}.xps");
document.GenerateXps(filePath);
Helpers.Helpers.OpenFileUsingDefaultProgram(filePath);
}
#endregion
#region Images
///
/// Generates the document as a series of images and returns them as a collection of byte arrays.
///
/// Optional settings to customize the generation process, such as image resolution, compression ratio, and more.
public static IEnumerable GenerateImages(this IDocument document, ImageGenerationSettings? settings = null)
{
settings ??= ImageGenerationSettings.Default;
return DocumentGenerator.GenerateImages(document, settings);
}
/// Specifies the index of the generated image from the document, starting at 0.
/// The file path where the image should be saved.
public delegate string GenerateDocumentImagePath(int imageIndex);
///
/// Generates the document as a sequence of images, saving them to paths determined by the delegate.
///
/// A delegate that gets image index as an input, and returns file path where it should be saved.
/// Optional settings for fine-tuning the generated images, such as resolution, compression ratio, etc.
public static void GenerateImages(this IDocument document, GenerateDocumentImagePath imagePathSource, ImageGenerationSettings? settings = null)
{
settings ??= ImageGenerationSettings.Default;
var index = 0;
foreach (var imageData in document.GenerateImages(settings))
{
var path = imagePathSource(index);
if (File.Exists(path))
File.Delete(path);
File.WriteAllBytes(path, imageData);
index++;
}
}
#endregion
#region SVG
///
/// Generates the document as a series of SVG images and returns them as a collection of strings.
///
public static ICollection GenerateSvg(this IDocument document)
{
return DocumentGenerator.GenerateSvg(document);
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Fluent/GridExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class GridDescriptor
{
internal Grid Grid { get; } = new Grid();
internal GridDescriptor()
{
}
public void Spacing(float value, Unit unit = Unit.Point)
{
VerticalSpacing(value, unit);
HorizontalSpacing(value, unit);
}
public void VerticalSpacing(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "The Grid vertical spacing cannot be negative.");
Grid.VerticalSpacing = value.ToPoints(unit);
}
public void HorizontalSpacing(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "The Grid horizontal spacing cannot be negative.");
Grid.HorizontalSpacing = value.ToPoints(unit);
}
public void Columns(int value = Grid.DefaultColumnsCount)
{
if (value < 1)
throw new ArgumentOutOfRangeException(nameof(value), "The Grid columns count cannot be less than 1.");
Grid.ColumnsCount = value;
}
public void Alignment(HorizontalAlignment alignment)
{
Grid.Alignment = alignment;
}
public void AlignLeft() => Alignment(HorizontalAlignment.Left);
public void AlignCenter() => Alignment(HorizontalAlignment.Center);
public void AlignRight() => Alignment(HorizontalAlignment.Right);
public IContainer Item(int columns = 1)
{
if (columns < 1)
throw new ArgumentOutOfRangeException(nameof(columns), "The Grid item cannot span less than 1 column.");
var container = new Container();
var element = new GridElement
{
Columns = columns,
Child = container
};
Grid.Children.Add(element);
return container;
}
}
public static class GridExtensions
{
[Obsolete("This element has been deprecated since version 2022.11. Please use the Table element, or the combination of the Row and Column elements.")]
[ExcludeFromCodeCoverage]
public static void Grid(this IContainer element, Action handler)
{
var descriptor = new GridDescriptor();
if (element is Alignment alignment && alignment.Horizontal.HasValue)
descriptor.Alignment(alignment.Horizontal.Value);
handler(descriptor);
element.Component(descriptor.Grid);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/ImageExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class DynamicImageDescriptor
{
private Elements.DynamicImage ImageElement { get; }
internal DynamicImageDescriptor(Elements.DynamicImage imageElement)
{
ImageElement = imageElement;
}
///
public DynamicImageDescriptor UseOriginalImage(bool value = true)
{
ImageElement.UseOriginalImage = value;
return this;
}
///
public DynamicImageDescriptor WithRasterDpi(int dpi)
{
if (dpi <= 0)
throw new DocumentComposeException("DPI value must be greater than 0.");
ImageElement.TargetDpi = dpi;
return this;
}
///
public DynamicImageDescriptor WithCompressionQuality(ImageCompressionQuality quality)
{
ImageElement.CompressionQuality = quality;
return this;
}
}
public sealed class ImageDescriptor
{
private Elements.Image ImageElement { get; }
private AspectRatio AspectRatioElement { get; }
private float ImageAspectRatio { get; }
internal ImageDescriptor(Elements.Image imageElement, Elements.AspectRatio aspectRatioElement)
{
ImageElement = imageElement;
AspectRatioElement = aspectRatioElement;
var imageSize = ImageElement.DocumentImage.Size;
ImageAspectRatio = imageSize.Width / (float)imageSize.Height;
}
///
public ImageDescriptor UseOriginalImage(bool value = true)
{
ImageElement.UseOriginalImage = value;
return this;
}
///
public ImageDescriptor WithRasterDpi(int dpi)
{
if (dpi <= 0)
throw new DocumentComposeException("DPI value must be greater than 0.");
ImageElement.TargetDpi = dpi;
return this;
}
///
public ImageDescriptor WithCompressionQuality(ImageCompressionQuality quality)
{
ImageElement.CompressionQuality = quality;
return this;
}
#region Aspect Ratio
///
/// Scales the image to fill the full width of its container. This is the default behavior.
///
public ImageDescriptor FitWidth()
{
return SetAspectRatio(AspectRatioOption.FitWidth);
}
///
/// The image stretches vertically to fit the full available height.
/// Often used with height-constraining elements such as: Height, MaxHeight, etc.
///
public ImageDescriptor FitHeight()
{
return SetAspectRatio(AspectRatioOption.FitHeight);
}
///
/// Combines the FitWidth and FitHeight settings.
/// The image resizes itself to utilize all available space, preserving its aspect ratio.
/// It will either fill the width or height based on the container's dimensions.
///
///
/// An optimal and safe choice.
///
public ImageDescriptor FitArea()
{
return SetAspectRatio(AspectRatioOption.FitArea);
}
///
/// The image adjusts to fill all the available space, disregarding its original proportions.
/// This can lead to distorted scaling and is generally not recommended for most scenarios.
///
public ImageDescriptor FitUnproportionally()
{
AspectRatioElement.Ratio = 0;
return this;
}
internal ImageDescriptor SetAspectRatio(AspectRatioOption option)
{
AspectRatioElement.Ratio = ImageAspectRatio;
AspectRatioElement.Option = option;
return this;
}
#endregion
}
public static class ImageExtensions
{
///
/// Draws an image by decoding it from a provided byte array.
/// Learn more
///
///
///
public static ImageDescriptor Image(this IContainer parent, byte[] imageData)
{
var image = Infrastructure.Image.FromBinaryData(imageData);
image.IsShared = false;
return parent.Image(image);
}
///
/// Draws the image loaded from a file located at the provided path.
/// Learn more
///
///
///
public static ImageDescriptor Image(this IContainer parent, string filePath)
{
var image = StaticImageCache.LoadFromCache(filePath);
return parent.Image(image);
}
///
/// Draws the image loaded from a stream.
/// Learn more
///
///
///
public static ImageDescriptor Image(this IContainer parent, Stream fileStream)
{
var image = Infrastructure.Image.FromStream(fileStream);
image.IsShared = false;
return parent.Image(image);
}
///
/// Draws the object. Allows to optimize the generation process.
/// Learn more
///
///
///
public static ImageDescriptor Image(this IContainer parent, Infrastructure.Image image)
{
if (image == null)
throw new DocumentComposeException("Cannot load or decode provided image.");
var imageElement = new QuestPDF.Elements.Image
{
DocumentImage = image
};
var aspectRationElement = new AspectRatio
{
Child = imageElement
};
parent.Element(aspectRationElement);
var bestScalingOption = GetBestAspectRatioOptionFromParent(parent);
return new ImageDescriptor(imageElement, aspectRationElement).SetAspectRatio(bestScalingOption);
}
internal static AspectRatioOption GetBestAspectRatioOptionFromParent(IContainer container)
{
if (container is not Constrained constrained)
return AspectRatioOption.FitWidth;
var hasWidthConstraint = constrained.MinWidth is not null || constrained.MaxWidth is not null;
var hasHeightConstraint = constrained.MinHeight is not null || constrained.MaxHeight is not null;
if (hasWidthConstraint && hasHeightConstraint)
return AspectRatioOption.FitArea;
if (hasWidthConstraint)
return AspectRatioOption.FitWidth;
if (hasHeightConstraint)
return AspectRatioOption.FitHeight;
return AspectRatioOption.FitWidth;
}
///
/// Renders an image of dynamic size dictated by the document layout constraints.
///
///
/// Ideal for generating pixel-perfect images that might lose quality upon scaling, such as maps or charts.
///
///
/// A delegate that requests an image of desired resolution calculated based on target physical image size and provided DPI.
///
/// A descriptor for adjusting image attributes like scaling behavior, compression quality, and resolution.
public static DynamicImageDescriptor Image(this IContainer element, GenerateDynamicImageDelegate dynamicImageSource)
{
var dynamicImage = new DynamicImage
{
Source = dynamicImageSource
};
element.Element(dynamicImage);
return new DynamicImageDescriptor(dynamicImage);
}
///
/// Renders an image of dynamic size dictated by the document layout constraints.
///
///
/// Ideal for generating pixel-perfect images that might lose quality upon scaling, such as maps or charts.
///
///
/// A delegate that requests an image of desired resolution calculated based on target physical image size and provided DPI.
///
/// A descriptor for adjusting image attributes like scaling behavior, compression quality, and resolution.
public static DynamicImageDescriptor Image(this IContainer element, Func dynamicImageSource)
{
var dynamicImage = new DynamicImage
{
Source = payload => dynamicImageSource(payload.ImageSize)
};
element.Element(dynamicImage);
return new DynamicImageDescriptor(dynamicImage);
}
#region Obsolete
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that takes the GenerateDynamicImageDelegate as an argument.")]
[ExcludeFromCodeCoverage]
public static void Image(this IContainer element, Func imageSource)
{
element.Image((ImageSize x) => imageSource(new Size(x.Width, x.Height)));
}
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
[ExcludeFromCodeCoverage]
public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling)
{
parent.Image(imageData).ApplyScaling(scaling);
}
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
[ExcludeFromCodeCoverage]
public static void Image(this IContainer parent, string filePath, ImageScaling scaling)
{
parent.Image(filePath).ApplyScaling(scaling);
}
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
[ExcludeFromCodeCoverage]
public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling)
{
parent.Image(fileStream).ApplyScaling(scaling);
}
internal static void ApplyScaling(this ImageDescriptor descriptor, ImageScaling scaling)
{
if (scaling == ImageScaling.Resize)
descriptor.FitUnproportionally();
else if (scaling == ImageScaling.FitWidth)
descriptor.FitWidth();
else if (scaling == ImageScaling.FitHeight)
descriptor.FitHeight();
else if (scaling == ImageScaling.FitArea)
descriptor.FitArea();
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Fluent/InlinedExtensions.cs
================================================
using System;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class InlinedDescriptor
{
internal Inlined Inlined { get; } = new Inlined();
internal InlinedDescriptor()
{
}
#region Spacing
///
/// Sets the vertical and horizontal gaps between items.
///
public void Spacing(float value, Unit unit = Unit.Point)
{
VerticalSpacing(value, unit);
HorizontalSpacing(value, unit);
}
///
/// Sets the vertical gaps between items.
///
public void VerticalSpacing(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "The Inlined vertical spacing cannot be negative.");
Inlined.VerticalSpacing = value.ToPoints(unit);
}
///
/// Sets the horizontal gaps between items.
///
public void HorizontalSpacing(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "The Inlined horizontal spacing cannot be negative.");
Inlined.HorizontalSpacing = value.ToPoints(unit);
}
#endregion
#region Baseline
///
/// Positions items vertically such that their top edges align on a single line.
///
public void BaselineTop()
{
Inlined.BaselineAlignment = VerticalAlignment.Top;
}
///
/// Positions items to have their centers in a straight horizontal line.
///
public void BaselineMiddle()
{
Inlined.BaselineAlignment = VerticalAlignment.Middle;
}
///
/// Positions items vertically such that their bottom edges align on a single line.
///
public void BaselineBottom()
{
Inlined.BaselineAlignment = VerticalAlignment.Bottom;
}
#endregion
#region Horizontal Alignment
internal void Alignment(InlinedAlignment? alignment)
{
Inlined.ElementsAlignment = alignment;
}
///
/// Aligns items horizontally to the left side.
///
public void AlignLeft()
{
Inlined.ElementsAlignment = InlinedAlignment.Left;
}
///
/// Aligns items horizontally to the center.
///
public void AlignCenter()
{
Inlined.ElementsAlignment = InlinedAlignment.Center;
}
///
/// Aligns items horizontally to the left right.
///
public void AlignRight()
{
Inlined.ElementsAlignment = InlinedAlignment.Right;
}
///
/// Distributes items horizontally, ensuring even spacing from edge to edge of the container.
///
public void AlignJustify()
{
Inlined.ElementsAlignment = InlinedAlignment.Justify;
}
///
/// Spaces items equally in a horizontal arrangement, both between items and at the ends.
///
public void AlignSpaceAround()
{
Inlined.ElementsAlignment = InlinedAlignment.SpaceAround;
}
#endregion
///
/// Adds a new item to the container.
///
/// The container of the newly created item.
public IContainer Item()
{
var container = new Constrained();
Inlined.Elements.Add(container);
return container;
}
}
public static class InlinedExtensions
{
///
/// Arranges its items sequentially in a line, wrapping to the next line if necessary.
/// Learn more
///
///
/// Supports the paging functionality.
///
/// Handler to configure content of this container, as well as spacing and items alignment.
public static void Inlined(this IContainer element, Action handler)
{
var descriptor = new InlinedDescriptor();
handler(descriptor);
element.Element(descriptor.Inlined);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/LayerExtensions.cs
================================================
using System;
using System.Linq;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class LayersDescriptor
{
internal Layers Layers { get; } = new Layers();
internal LayersDescriptor()
{
}
private IContainer Layer(bool isPrimary)
{
var container = new Container();
var element = new Layer
{
IsPrimary = isPrimary,
Child = container
};
Layers.Children.Add(element);
return container;
}
///
/// Specifies an additional layer for the container.
///
///
/// The order of code execution determines the drawing order:
/// If the layer is defined before the primary layer, it's drawn underneath the primary content (as a background).
/// If defined after the primary layer, it's drawn in front of the primary content (as a watermark).
///
public IContainer Layer() => Layer(false);
///
/// Sets the primary content for the container.
///
///
/// Exactly one primary layer should be defined.
///
public IContainer PrimaryLayer() => Layer(true);
internal void Validate()
{
var primaryLayers = Layers.Children.Count(x => x.IsPrimary);
if (primaryLayers == 0)
throw new DocumentComposeException("The Layers component needs to have exactly one primary layer. It has none.");
if (primaryLayers != 1)
throw new DocumentComposeException($"The Layers component needs to have exactly one primary layer. It has {primaryLayers}.");
}
}
public static class LayerExtensions
{
///
/// Adds content either underneath (as a background) or on top of (as a watermark) the main content.
/// The main layer supports paging, can span multiple pages, and determines the container's target length.
/// Additional layers can also span multiple pages and are repeated on each one.
/// Learn more
///
/// Handler for defining content of the container, including exactly one primary layer and any additional layers in a specified order.
public static void Layers(this IContainer element, Action handler)
{
var descriptor = new LayersDescriptor();
handler(descriptor);
descriptor.Validate();
element.Element(descriptor.Layers);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/LineExtensions.cs
================================================
using System;
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class LineDescriptor
{
internal Line Line { get; } = new Line();
internal LineDescriptor()
{
}
///
/// Specifies the color for the line.
///
///
public LineDescriptor LineColor(Color color)
{
Line.Color = color;
return this;
}
///
/// Configures a dashed pattern for the line.
/// For example, a pattern of [2, 3] creates a dash of 2 units followed by a gap of 3 units.
///
/// The length of this array must be even.
public LineDescriptor LineDashPattern(float[] dashPattern, Unit unit = Unit.Point)
{
if (dashPattern == null)
throw new ArgumentNullException(nameof(dashPattern), "The dash pattern cannot be null.");
if (dashPattern.Length == 0)
throw new ArgumentException("The dash pattern cannot be empty.", nameof(dashPattern));
if (dashPattern.Length % 2 != 0)
throw new ArgumentException("The dash pattern must contain an even number of elements.", nameof(dashPattern));
Line.DashPattern = dashPattern.Select(x => x.ToPoints(unit)).ToArray();
return this;
}
///
/// Applies a linear gradient to a line using the specified colors.
///
public LineDescriptor LineGradient(Color[] colors)
{
if (colors == null)
throw new ArgumentNullException(nameof(colors), "The gradient colors cannot be null.");
if (colors.Length == 0)
throw new ArgumentException("The gradient colors cannot be empty.", nameof(colors));
Line.GradientColors = colors;
return this;
}
}
public static class LineExtensions
{
private static LineDescriptor Line(this IContainer element, LineType type, float thickness)
{
if (thickness < 0)
throw new ArgumentOutOfRangeException(nameof(thickness), "The Line thickness cannot be negative.");
var descriptor = new LineDescriptor();
descriptor.Line.Thickness = thickness;
descriptor.Line.Type = type;
element.Element(descriptor.Line);
return descriptor;
}
///
/// Renders a vertical line with a specified thickness.
/// Learn more
///
///
/// The line is not just a visual element; it occupies actual space within the document.
///
/// A descriptor to modify line attributes.
public static LineDescriptor LineVertical(this IContainer element, float thickness, Unit unit = Unit.Point)
{
return element.Line(LineType.Vertical, thickness.ToPoints(unit));
}
///
/// Renders a horizontal line with a specified thickness.
/// Learn more
///
///
/// The line is not just a visual element; it occupies actual space within the document.
///
/// A descriptor to modify line attributes.
public static LineDescriptor LineHorizontal(this IContainer element, float thickness, Unit unit = Unit.Point)
{
return element.Line(LineType.Horizontal, thickness.ToPoints(unit));
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/MinimalApi.cs
================================================
using System;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Fluent
{
public sealed class Document : IDocument
{
static Document()
{
SkNativeDependencyCompatibilityChecker.Test();
}
private Action ContentSource { get; }
private DocumentMetadata Metadata { get; set; } = DocumentMetadata.Default;
private DocumentSettings Settings { get; set; } = DocumentSettings.Default;
private Document(Action contentSource)
{
ContentSource = contentSource;
}
///
/// Creates a new empty document and provides handler to specify its content.
///
/// A Document object with the specified content. This object allows to set metadata, configure generation parameters, and produce output files such as PDF, XPS, or images.
public static Document Create(Action handler)
{
return new Document(handler);
}
///
/// Configures the metadata of the PDF document, such as title, author, keywords, etc.
///
public Document WithMetadata(DocumentMetadata metadata)
{
Metadata = metadata ?? Metadata;
return this;
}
///
/// Enables fine-tuning of the document generation process, influencing attributes of the resulting PDF such as target DPI, image compression, compliance with the PDF/A standard, etc.
///
public Document WithSettings(DocumentSettings settings)
{
Settings = settings ?? Settings;
return this;
}
///
/// Combines multiple documents together into a single one.
///
/// A MergedDocument object that allows to set metadata, configure generation parameters, adjust merging strategy, and produce output files such as PDF, XPS, or images.
public static MergedDocument Merge(IEnumerable documents)
{
return new MergedDocument(documents);
}
///
/// Combines multiple documents together into a single one.
///
/// A MergedDocument object that allows to set metadata, configure generation parameters, adjust merging strategy, and produce output files such as PDF, XPS, or images.
public static MergedDocument Merge(params IDocument[] documents)
{
return new MergedDocument(documents);
}
#region IDocument
///
/// Implements the IDocument interface. Don't use within the Fluent API chain.
///
public DocumentMetadata GetMetadata() => Metadata;
///
/// Implements the IDocument interface. Don't use within the Fluent API chain.
///
public DocumentSettings GetSettings() => Settings;
///
/// Implements the IDocument interface. Don't use within the Fluent API chain.
///
public void Compose(IDocumentContainer container) => ContentSource(container);
#endregion
}
}
================================================
FILE: Source/QuestPDF/Fluent/MultiColumnExtensions.cs
================================================
using System;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Fluent;
public sealed class MultiColumnDescriptor
{
internal MultiColumn MultiColumn { get; } = new MultiColumn();
internal MultiColumnDescriptor()
{
}
///
/// Configures the horizontal spacing between adjacent columns in the layout.
///
///
/// This method affects the visual presentation of the column arrangement.
/// Positive values increase separation, while negative values may cause overlap.
///
public void Spacing(float value, Unit unit = Unit.Point)
{
MultiColumn.Spacing = value.ToPoints(unit);
}
///
/// Defines the number of vertical columns in the layout.
///
///
/// This method determines the basic structure of the grid layout.
/// Setting this value will redistribute the existing elements across the new column count.
///
public void Columns(int value = 2)
{
if (value < 2)
throw new DocumentComposeException("The 'MultiColumn.Columns' value should be higher than 1.");
MultiColumn.ColumnCount = value;
}
///
/// Controls the content distribution across columns to achieve balanced heights.
///
///
/// When enabled: content flow is adjusted to equalize column heights; each column will have approximately the same height.
/// When disabled: layout occupies the full vertical space, the last column may be shorter or empty, depending on content quantity.
///
public void BalanceHeight(bool enable = true)
{
MultiColumn.BalanceHeight = enable;
}
///
/// Retrieves a container for the primary content to be distributed across multiple columns.
///
///
/// The returned container serves as the main content area for the multi-column layout.
/// It supports all available layout elements and automatically divides its content among the defined columns.
///
public IContainer Content()
{
if (MultiColumn.Content is not Empty)
throw new DocumentComposeException("The 'MultiColumn.Content' layer has already been defined. Please call this method only once.");
var container = new Container();
MultiColumn.Content = container;
return container;
}
///
/// Retrieves a container for content positioned between columns in the layout.
///
///
/// The container's dimensions are determined by the height of the columns and the configured spacing.
/// This container supports all available layout elements, allowing for flexible design of inter-column content.
///
public IContainer Spacer()
{
if (MultiColumn.Spacer is not Empty)
throw new DocumentComposeException("The 'MultiColumn.Spacer' layer has already been defined. Please call this method only once.");
var container = new Container();
MultiColumn.Spacer = container;
return container
.Artifact(SkSemanticNodeSpecialId.LayoutArtifact)
.Repeat();
}
}
public static class MultiColumnExtensions
{
///
/// Creates a multi-column layout within the current container element.
///
///
/// A multi-column layout organizes content into vertical columns, similar to a newspaper or magazine layout.
/// This approach allows for efficient use of horizontal space and can improve readability, especially
/// for wide containers or screens. The content flows from one column to the next, and the number of
/// columns can be adjusted based on the container's width or specific design requirements.
///
/// The action to configure the column's content and behavior.
public static void MultiColumn(this IContainer element, Action handler)
{
var descriptor = new MultiColumnDescriptor();
handler(descriptor);
element
.Element(x => descriptor.MultiColumn.BalanceHeight ? x.ShrinkVertical() : x)
.Element(descriptor.MultiColumn);
}
}
================================================
FILE: Source/QuestPDF/Fluent/PaddingExtensions.cs
================================================
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class PaddingExtensions
{
private static IContainer Padding(this IContainer element, float top = 0, float bottom = 0, float left = 0, float right = 0)
{
var padding = element as Padding ?? new Padding();
padding.Top += top;
padding.Bottom += bottom;
padding.Left += left;
padding.Right += right;
return element.Element(padding);
}
///
/// For positive values, adds empty space around its content.
/// For negative values, pushes its content beyond the edges, increasing available space, similarly to negative HTML margins.
///
/// Learn more
///
public static IContainer Padding(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Padding(top: value, bottom: value, left: value, right: value);
}
///
/// For positive values, adds empty space horizontally (left and right) around its content.
/// For negative values, pushes its content beyond the horizontal edges, increasing available space, similarly to negative HTML margins.
///
/// Learn more
///
public static IContainer PaddingHorizontal(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Padding(left: value, right: value);
}
///
/// For positive values, adds empty space vertically (top and bottom) around its content.
/// For negative values, pushes its content beyond the vertical edges, increasing available space, similarly to negative HTML margins.
///
/// Learn more
///
public static IContainer PaddingVertical(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Padding(top: value, bottom: value);
}
///
/// For positive values, adds empty space above its content.
/// For negative values, pushes its content beyond the top edge, increasing available space, similarly to negative HTML margins.
///
/// Learn more
///
public static IContainer PaddingTop(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Padding(top: value);
}
///
/// For positive values, adds empty space below its content.
/// For negative values, pushes its content beyond the bottom edge, increasing available space, similarly to negative HTML margins.
///
/// Learn more
///
public static IContainer PaddingBottom(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Padding(bottom: value);
}
///
/// For positive values, adds empty space to the left of its content.
/// For negative values, pushes its content beyond the left edge, increasing available space, similarly to negative HTML margins.
///
/// Learn more
///
public static IContainer PaddingLeft(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Padding(left: value);
}
///
/// For positive values, adds empty space to the right of its content.
/// For negative values, pushes its content beyond the right edge, increasing available space, similarly to negative HTML margins.
///
/// Learn more
///
public static IContainer PaddingRight(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Padding(right: value);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/PageExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class PageDescriptor
{
internal Page Page { get; } = new Page();
internal PageDescriptor()
{
}
#region Size
///
/// Configures the dimensions of every page within the set.
///
public void Size(float width, float height, Unit unit = Unit.Point)
{
var pageSize = new PageSize(width, height, unit);
MinSize(pageSize);
MaxSize(pageSize);
}
///
/// Configures the dimensions of every page within the set.
///
public void Size(PageSize pageSize)
{
MinSize(pageSize);
MaxSize(pageSize);
}
///
/// Enables the continuous page size mode, allowing the page's height to adjust according to content while retaining a constant specified .
///
///
/// This configuration is suitable for producing PDFs intended for printing on the paper roll.
///
public void ContinuousSize(float width, Unit unit = Unit.Point)
{
MinSize(new PageSize(width.ToPoints(unit), 0));
MaxSize(new PageSize(width.ToPoints(unit), Infrastructure.Size.Max.Height));
}
///
/// Enables the flexible page size mode, where the output page's dimensions can vary based on its content.
/// Specifies the smallest possible page size.
///
///
/// Note that with this setting, individual pages within the document may have different sizes.
///
public void MinSize(PageSize pageSize)
{
Page.MinSize = pageSize;
}
///
/// Enables the flexible page size mode, where the output page's dimensions can vary based on its content.
/// Specifies the largest possible page size.
///
///
/// Note that with this setting, individual pages within the document may have different sizes.
///
public void MaxSize(PageSize pageSize)
{
Page.MaxSize = pageSize;
}
#endregion
#region Margin
///
/// Adds empty space to the left of the primary layer (header + content + footer).
///
public void MarginLeft(float value, Unit unit = Unit.Point)
{
Page.MarginLeft = value.ToPoints(unit);
}
///
/// Adds empty space to the right of the primary layer (header + content + footer).
///
public void MarginRight(float value, Unit unit = Unit.Point)
{
Page.MarginRight = value.ToPoints(unit);
}
///
/// Adds empty space above the primary layer (header + content + footer).
///
public void MarginTop(float value, Unit unit = Unit.Point)
{
Page.MarginTop = value.ToPoints(unit);
}
///
/// Adds empty space below the primary layer (header + content + footer).
///
public void MarginBottom(float value, Unit unit = Unit.Point)
{
Page.MarginBottom = value.ToPoints(unit);
}
///
/// Adds empty space vertically (top and bottom) around the primary layer (header + content + footer).
///
public void MarginVertical(float value, Unit unit = Unit.Point)
{
MarginTop(value, unit);
MarginBottom(value, unit);
}
///
/// Adds empty space horizontally (left and right) around the primary layer (header + content + footer).
///
public void MarginHorizontal(float value, Unit unit = Unit.Point)
{
MarginLeft(value, unit);
MarginRight(value, unit);
}
///
/// Adds empty space around the primary layer (header + content + footer).
///
public void Margin(float value, Unit unit = Unit.Point)
{
MarginVertical(value, unit);
MarginHorizontal(value, unit);
}
#endregion
#region Properties
///
/// Applies a default text style to all Text elements within the page set.
///
///
/// Use this method to achieve consistent text styling across entire document.
///
/// A delegate to adjust the global text style attributes.
public void DefaultTextStyle(TextStyle textStyle)
{
Page.DefaultTextStyle = textStyle;
}
///
/// Applies a default text style to all Text elements within the page set.
///
///
/// Use this method to achieve consistent text styling across entire document.
///
/// A handler to adjust the global text style attributes.
public void DefaultTextStyle(Func handler)
{
DefaultTextStyle(handler(TextStyle.Default));
}
///
/// Applies a left-to-right (LTR) content direction to all elements within the page set.
/// Learn more
///
///
public void ContentFromLeftToRight()
{
Page.ContentDirection = ContentDirection.LeftToRight;
}
///
/// Applies a right-to-left (RTL) content direction to all elements within the page set.
/// Learn more
///
///
public void ContentFromRightToLeft()
{
Page.ContentDirection = ContentDirection.RightToLeft;
}
///
/// Sets a background color of the page, which is white by default.
///
///
/// When working with file formats that support the alpha channel, it is possible to set the color to if necessary.
///
///
public void PageColor(Color color)
{
Page.BackgroundColor = color;
}
[Obsolete("This element has been renamed since version 2022.3. Please use the PageColor method.")]
[ExcludeFromCodeCoverage]
public void Background(Color color)
{
PageColor(color);
}
#endregion
#region Slots
///
/// Represents a layer drawn behind the primary layer (header + content + footer), serving as a background.
///
///
/// Unaffected by the Margin configuration, it always occupies entire page.
///
public IContainer Background()
{
if (Page.Background is not Empty)
throw new DocumentComposeException("The 'Page.Background' layer has already been defined. Please call this method only once.");
var container = new Container();
Page.Background = container;
return container;
}
///
/// Represents a layer drawn in front of the primary layer (header + content + footer), serving as a watermark.
///
///
/// Unaffected by the Margin configuration, it always occupies entire page.
///
public IContainer Foreground()
{
if (Page.Foreground is not Empty)
throw new DocumentComposeException("The 'Page.Foreground' layer has already been defined. Please call this method only once.");
var container = new Container();
Page.Foreground = container;
return container;
}
///
/// Represents the segment at the very top of the page, just above the main content.
///
///
/// This container does not support paging capability. It's expected to be fully displayed on every page.
///
public IContainer Header()
{
if (Page.Header is not Empty)
throw new DocumentComposeException("The 'Page.Header' layer has already been defined. Please call this method only once.");
var container = new Container();
Page.Header = container;
return container;
}
///
/// Represents the primary content, located in-between the header and footer.
///
///
/// This container does support paging capability and determines the final lenght of the document.
///
public IContainer Content()
{
if (Page.Content is not Empty)
throw new DocumentComposeException("The 'Page.Content' layer has already been defined. Please call this method only once.");
var container = new Container();
Page.Content = container;
return container;
}
///
/// Represents the section at the very bottom of the page, just below the main content.
///
///
/// This container does not support paging capability. It's expected to be fully displayed on every page.
///
public IContainer Footer()
{
if (Page.Footer is not Empty)
throw new DocumentComposeException("The 'Page.Footer' layer has already been defined. Please call this method only once.");
var container = new Container();
Page.Footer = container;
return container;
}
#endregion
}
public static class PageExtensions
{
///
/// Creates a new page set with consistent attributes, such as margin, color, and watermark.
/// The length of each set depends on its content.
///
///
/// By leveraging multiple page sets, you can produce documents containing pages of distinct sizes and characteristics.
///
/// Delegate to define page content (layout and visual elements).
/// Continuation of the Document API chain, permitting the definition of other page sets.
public static IDocumentContainer Page(this IDocumentContainer document, Action handler)
{
var descriptor = new PageDescriptor();
handler(descriptor);
(document as DocumentContainer).Pages.Add(descriptor.Page);
return document;
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/RotateExtensions.cs
================================================
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class RotateExtensions
{
private static IContainer SimpleRotate(this IContainer element, int turnDirection)
{
var scale = element as SimpleRotate ?? new SimpleRotate();
scale.TurnCount += turnDirection;
return element.Element(scale);
}
///
/// Rotates its content 90 degrees counterclockwise.
/// Learn more
///
///
/// Note: Rotation can alter certain attributes; for example, 'width' might effectively become 'height'.
///
public static IContainer RotateLeft(this IContainer element)
{
return element.SimpleRotate(-1);
}
///
/// Rotates its content 90 degrees clockwise.
/// Learn more
///
///
/// Note: Rotation can alter certain attributes; for example, 'width' might effectively become 'height'.
///
public static IContainer RotateRight(this IContainer element)
{
return element.SimpleRotate(1);
}
///
/// Rotates its content clockwise by a given angle.
/// Learn more
///
/// Rotation angle in degrees. A value of 360 degrees represents a full rotation.
public static IContainer Rotate(this IContainer element, float angle)
{
var scale = element as Rotate ?? new Rotate();
scale.Angle += angle;
return element.Element(scale);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/RowExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class RowDescriptor
{
internal Row Row { get; } = new();
internal RowDescriptor()
{
}
///
/// Adjusts horizontal spacing between items.
///
public void Spacing(float spacing, Unit unit = Unit.Point)
{
if (spacing < 0)
throw new ArgumentOutOfRangeException(nameof(spacing), "The row spacing cannot be negative.");
Row.Spacing = spacing.ToPoints(unit);
}
private IContainer Item(RowItemType type, float size = 0)
{
var item = new RowItem
{
Type = type,
Size = size
};
Row.Items.Add(item);
return item;
}
[Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")]
[ExcludeFromCodeCoverage]
public IContainer RelativeColumn(float size = 1)
{
return Item(RowItemType.Relative, size);
}
[Obsolete("This element has been renamed since version 2022.2. Please use the ConstantItem method.")]
[ExcludeFromCodeCoverage]
public IContainer ConstantColumn(float size)
{
return Item(RowItemType.Constant, size);
}
///
/// Adds a new item to the row element. This item occupies space proportionally to other relative items.
///
///
/// For a row element with a width of 100 points that has three items (a relative item of size 1, a relative item of size 5, and a constant item of size 10 points),
/// the items will occupy sizes of 15 points, 75 points, and 10 points respectively.
///
/// The container of the newly added item.
public IContainer RelativeItem(float size = 1)
{
if (size <= 0)
throw new ArgumentOutOfRangeException(nameof(size), "The relative item size must be greater than zero.");
return Item(RowItemType.Relative, size);
}
///
/// Adds a new item to the row element with a specified constant size.
///
/// The container of the newly created item.
public IContainer ConstantItem(float size, Unit unit = Unit.Point)
{
if (size < 0)
throw new ArgumentOutOfRangeException(nameof(size), "The constant item size cannot be negative.");
return Item(RowItemType.Constant, size.ToPoints(unit));
}
///
/// Adds a new item to the row element. The size of this item adjusts based on its content.
///
///
/// The AutoItem requests as much horizontal space as its content requires.
/// It doesn't adjust its size based on other items and may frequently result in a .
/// It's recommended to use this API in conjunction with the MaxWidth element.
///
/// The container of the newly created item.
public IContainer AutoItem()
{
return Item(RowItemType.Auto);
}
}
public static class RowExtensions
{
///
/// Draws a collection of elements horizontally.
/// Depending on the content direction mode, elements will be drawn from left to right, or from right to left.
///
/// Learn more
///
///
/// Supports paging.
/// Depending on its content, the Row element may repeatedly draw certain items across multiple pages. Use the ShowOnce element to modify this behavior if it's not desired.
///
/// The action to configure the row's content.
public static void Row(this IContainer element, Action handler)
{
var descriptor = new RowDescriptor();
handler(descriptor);
element.Element(descriptor.Row);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/ScaleExtensions.cs
================================================
using System;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class ScaleExtensions
{
private static IContainer ScaleValue(this IContainer element, float x = 1, float y = 1)
{
var scale = element as Scale ?? new Scale();
scale.ScaleX *= x;
scale.ScaleY *= y;
return element.Element(scale);
}
///
/// Scales its inner content proportionally.
/// Learn more
///
///
///
public static IContainer Scale(this IContainer element, float factor)
{
if (factor == 0)
throw new ArgumentException("Vertical scale factor cannot be zero.", nameof(factor));
return element.ScaleValue(x: factor, y: factor);
}
///
/// Scales the available horizontal space (along the X axis), causing content to appear expanded or squished, rather than simply larger or smaller.
/// Learn more
///
///
///
public static IContainer ScaleHorizontal(this IContainer element, float factor)
{
if (factor == 0)
throw new ArgumentException("Vertical scale factor cannot be zero.", nameof(factor));
return element.ScaleValue(x: factor);
}
///
/// Scales the available vertical space (along the Y axis), causing content to appear expanded or squished, rather than simply larger or smaller.
/// Learn more
///
///
///
public static IContainer ScaleVertical(this IContainer element, float factor)
{
if (factor == 0)
throw new ArgumentException("Vertical scale factor cannot be zero.", nameof(factor));
return element.ScaleValue(y: factor);
}
///
/// Flips its content to create a mirror image along the Y axis, swapping elements from left to right.
/// Learn more
///
///
/// Elements on the left will appear on the right.
///
public static IContainer FlipHorizontal(this IContainer element)
{
return element.ScaleValue(x: -1);
}
///
/// Flips its content to create a mirror image along the X axis, moving elements from the top to the bottom.
/// Learn more
///
///
/// Elements at the top will be positioned at the bottom.
///
public static IContainer FlipVertical(this IContainer element)
{
return element.ScaleValue(y: -1);
}
///
/// Creates a mirror image of its content across both axes.
/// Learn more
///
///
/// Elements originally in the top-left corner will be positioned in the bottom-right corner.
///
public static IContainer FlipOver(this IContainer element)
{
return element.ScaleValue(x: -1, y: -1);
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/SemanticExtensions.cs
================================================
using System;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
using QuestPDF.Skia;
namespace QuestPDF.Fluent;
public static class SemanticExtensions
{
internal static IContainer Artifact(this IContainer container, int nodeId)
{
return container.Element(new Elements.ArtifactTag
{
Id = nodeId
});
}
///
/// Excludes the container content from the semantic tree.
/// Use for decorative elements, layout artifacts, or other non-structural content that shouldn't be part of the document's logical structure.
///
public static IContainer SemanticIgnore(this IContainer container)
{
return container.Artifact(SkSemanticNodeSpecialId.OtherArtifact);
}
internal static IContainer SemanticTag(this IContainer container, string type, string? alternativeText = null, string? language = null)
{
return container.Element(new Elements.SemanticTag
{
TagType = type,
Alt = alternativeText,
Lang = language
});
}
///
/// Marks a self-contained body of text that forms a single narrative or exposition,
/// such as a blog post, news story, or forum post.
/// As a best practice, articles should not be nested within each other.
///
public static IContainer SemanticArticle(this IContainer container)
{
return container.SemanticTag("Art");
}
///
/// Applies a 'Section' tag, grouping a set of related content.
/// A section typically includes a heading (e.g., SemanticHeader2) and its corresponding content.
/// Sections can be nested to create a hierarchical document structure.
///
public static IContainer SemanticSection(this IContainer container)
{
return container.SemanticTag("Sect");
}
///
/// Marks a generic block-level container for grouping elements.
/// It's often used when a more specific semantic tag (like 'Article' or 'Section') doesn't apply,
/// serving as a general-purpose 'div', similar to its HTML counterpart.
///
public static IContainer SemanticDivision(this IContainer container)
{
return container.SemanticTag("Div");
}
///
/// Designates a block of text that is a quotation, typically consisting of one or more paragraphs.
/// This is for block-level quotes, as opposed to which is for inline text.
///
public static IContainer SemanticBlockQuotation(this IContainer container)
{
return container.SemanticTag("BlockQuote");
}
///
/// Identifies a brief portion of text that serves as a caption or description
/// for a table, figure, or image. It should be placed near the element it describes.
///
public static IContainer SemanticCaption(this IContainer container)
{
return container.SemanticTag("Caption");
}
///
/// Marks a section of the document as an index.
/// This container typically holds a sequence of entries and references.
///
public static IContainer SemanticIndex(this IContainer container)
{
return container.SemanticTag("Index");
}
///
/// Applies a language attribute to a container, specifying the natural language (e.g., 'en-US', 'es-ES') of its content.
/// This is crucial for accessibility, enabling screen readers to use the correct pronunciation.
///
/// The ISO 639 language code (e.g., 'en-US' or 'fr-FR') for the content.
public static IContainer SemanticLanguage(this IContainer container, string language)
{
return container.SemanticTag("NonStruct", language: language);
}
#region Table of Contents
///
/// Marks a container as a Table of Contents (TOC).
///
/// A TOC should be composed of elements.
/// TOCs can be nested to represent a hierarchical document structure.
///
/// This tag can also be used for lists of figures, lists of tables, or bibliographies.
///
public static IContainer SemanticTableOfContents(this IContainer container)
{
return container.SemanticTag("TOC");
}
///
/// Marks an individual item within a .
/// This typically represents a single entry in the list.
///
public static IContainer SemanticTableOfContentsItem(this IContainer container)
{
return container.SemanticTag("TOCI");
}
#endregion
#region Headers
private static IContainer SemanticHeader(this IContainer container, int level)
{
if (level < 1 || level > 6)
throw new ArgumentOutOfRangeException(nameof(level), "Header level must be between 1 and 6.");
return container.SemanticTag($"H{level}");
}
///
/// Marks the content as a level 1 heading (H1), the highest level in the document hierarchy.
/// Headings are crucial for navigation and outlining the document's structure.
///
public static IContainer SemanticHeader1(this IContainer container)
{
return container.SemanticHeader(1);
}
///
/// Marks the content as a level 2 heading (H2).
///
public static IContainer SemanticHeader2(this IContainer container)
{
return container.SemanticHeader(2);
}
///
/// Marks the content as a level 3 heading (H3).
///
public static IContainer SemanticHeader3(this IContainer container)
{
return container.SemanticHeader(3);
}
///
/// Marks the content as a level 4 heading (H4).
///
public static IContainer SemanticHeader4(this IContainer container)
{
return container.SemanticHeader(4);
}
///
/// Marks the content as a level 5 heading (H5).
///
public static IContainer SemanticHeader5(this IContainer container)
{
return container.SemanticHeader(5);
}
///
/// Marks the content as a level 6 heading (H6), the lowest level in the document hierarchy.
///
public static IContainer SemanticHeader6(this IContainer container)
{
return container.SemanticHeader(6);
}
#endregion
///
/// Marks a container as a paragraph.
/// This is one of the most common block-level tags for organizing text content.
///
public static IContainer SemanticParagraph(this IContainer container)
{
return container.SemanticTag("P");
}
#region Lists
///
/// Marks a container as a list.
/// Its direct children should be one or more elements.
/// A can also be included as an optional first child.
///
public static IContainer SemanticList(this IContainer container)
{
return container.SemanticTag("L");
}
///
/// Marks an individual item within a .
/// Its children should typically be a (e.g., the bullet or number) and/or a (the content).
///
public static IContainer SemanticListItem(this IContainer container)
{
return container.SemanticTag("LI");
}
///
/// Marks the label of a list item.
/// This container holds the bullet, number (e.g., '1.'), or term (in a definition list) that identifies the list item.
///
public static IContainer SemanticListLabel(this IContainer container)
{
return container.SemanticTag("Lbl");
}
///
/// Marks the body or descriptive content of a .
/// This contains the main text or content associated with the list item's label.
///
public static IContainer SemanticListItemBody(this IContainer container)
{
return container.SemanticTag("LBody");
}
#endregion
#region Table
///
/// Marks a container as a table.
/// The library automatically automatically tags headers, rows, cells, etc.
///
public static IContainer SemanticTable(this IContainer container)
{
return container.SemanticTag("Table");
}
#endregion
#region Inline Elements
///
/// Marks a generic inline portion of text (Span).
/// This is useful for grouping inline elements or applying styling, similar to an HTML <span>.
///
/// Optional alternative text, often used to provide an expansion for an abbreviation or other supplementary information.
public static IContainer SemanticSpan(this IContainer container, string? alternativeText = null)
{
return container.SemanticTag("Span", alternativeText);
}
///
/// Marks an inline portion of text as a quote.
/// This differs from , which is intended for block-level content (one or more paragraphs).
///
public static IContainer SemanticQuote(this IContainer container)
{
return container.SemanticTag("Quote");
}
///
/// Marks a fragment of text as computer code.
///
public static IContainer SemanticCode(this IContainer container)
{
return container.SemanticTag("Code");
}
///
/// Marks the content as a hyperlink (Link).
///
/// Alternative text describing the link's purpose or destination. This is essential for screen readers.
public static IContainer SemanticLink(this IContainer container, string alternativeText)
{
return container.SemanticTag("Link", alternativeText: alternativeText);
}
#endregion
#region Illustration Elements
///
/// Marks a container as a figure, which is an item of graphical content like a chart, diagram, or photograph.
///
/// A textual description of the figure, read by screen readers. This is essential for accessibility.
public static IContainer SemanticFigure(this IContainer container, string alternativeText)
{
return container.SemanticTag("Figure", alternativeText: alternativeText);
}
///
/// An alias for . Marks the content as an image.
///
/// A textual description of the image, read by screen readers. This is essential for accessibility.
public static IContainer SemanticImage(this IContainer container, string alternativeText)
{
return container.SemanticFigure(alternativeText);
}
///
/// Marks the content as a mathematical formula.
/// From a structural and accessibility standpoint, it is treated similarly to a figure.
///
public static IContainer SemanticFormula(this IContainer container, string alternativeText)
{
return container.SemanticTag("Formula", alternativeText: alternativeText);
}
#endregion
}
================================================
FILE: Source/QuestPDF/Fluent/ShrinkExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class ShrinkExtensions
{
private static IContainer Shrink(this IContainer element, bool? vertical = null, bool? horizontal = null)
{
var shrink = element as Shrink ?? new Shrink();
if (vertical.HasValue)
shrink.Vertical = vertical.Value;
if (horizontal.HasValue)
shrink.Horizontal = horizontal.Value;
return element.Element(shrink);
}
///
/// Renders its content in the most compact size achievable.
/// Ideal for situations where the parent element provides more space than necessary.
///
/// Learn more
///
public static IContainer Shrink(this IContainer element)
{
return element.Shrink(vertical: true, horizontal: true);
}
///
/// Minimizes content height to the minimum, optimizing vertical space.
/// Ideal for situations where the parent element provides more space than necessary.
///
/// Learn more
///
public static IContainer ShrinkVertical(this IContainer element)
{
return element.Shrink(vertical: true);
}
///
/// Minimizes content width to the minimum, optimizing horizontal space.
/// Ideal for situations where the parent element provides more space than necessary.
///
/// Learn more
///
public static IContainer ShrinkHorizontal(this IContainer element)
{
return element.Shrink(horizontal: true);
}
#region Obsolete
[Obsolete("This element has been renamed since version 2022.1. Please use the Shrink method.")]
[ExcludeFromCodeCoverage]
public static IContainer Box(this IContainer element)
{
return element.Shrink();
}
[Obsolete("This element has been renamed since version 2023.11. Please use the Shrink method.")]
[ExcludeFromCodeCoverage]
public static IContainer MinimalBox(this IContainer element)
{
return element.Shrink();
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Fluent/StyledBoxExtensions.cs
================================================
using System;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class StyledBoxExtensions
{
///
/// Sets a uniform border around the container with the specified thickness and color.
///
///
public static IContainer Border(this IContainer element, float all, Color color)
{
return element.Border(left: all, top: all, right: all, bottom: all).BorderColor(color);
}
///
/// Sets a solid background color behind its content.
/// Learn more
///
///
public static IContainer Background(this IContainer element, Color color)
{
var styledBox = element as StyledBox ?? new StyledBox();
styledBox.BackgroundColor = color;
return element.Element(styledBox);
}
///
/// Applies a linear gradient background to the container with the specified angle and colors.
///
/// The angle in degrees of the gradient direction.
/// An array of representing the gradient colors.
public static IContainer BackgroundLinearGradient(this IContainer element, float angle, Color[] colors)
{
if (colors == null || colors.Length == 0)
throw new ArgumentException("The background linear-gradient colors cannot be empty.", nameof(colors));
var border = element as StyledBox ?? new StyledBox();
border.BackgroundGradientAngle = angle;
border.BackgroundGradientColors = colors;
return element.Element(border);
}
#region Thickness
private static IContainer Border(this IContainer element, float? top = null, float? bottom = null, float? left = null, float? right = null)
{
var styledBox = element as StyledBox ?? new StyledBox();
if (top < 0)
throw new ArgumentOutOfRangeException(nameof(top), "The top border cannot be negative.");
if (bottom < 0)
throw new ArgumentOutOfRangeException(nameof(bottom), "The bottom border cannot be negative.");
if (left < 0)
throw new ArgumentOutOfRangeException(nameof(left), "The left border cannot be negative.");
if (right < 0)
throw new ArgumentOutOfRangeException(nameof(right), "The right border cannot be negative.");
if (styledBox.BorderColor == Colors.Transparent.Hex)
styledBox.BorderColor = Colors.Black;
if (top.HasValue)
styledBox.BorderTop = top.Value;
if (bottom.HasValue)
styledBox.BorderBottom = bottom.Value;
if (left.HasValue)
styledBox.BorderLeft = left.Value;
if (right.HasValue)
styledBox.BorderRight = right.Value;
return element.Element(styledBox);
}
///
/// Sets a uniform border (all edges) for its content.
/// Learn more
///
public static IContainer Border(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Border(top: value, bottom: value, left: value, right: value);
}
///
/// Sets a vertical border (left and right) for its content.
/// Learn more
///
public static IContainer BorderVertical(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Border(left: value, right: value);
}
///
/// Sets a horizontal border (top and bottom) for its content.
/// Learn more
///
public static IContainer BorderHorizontal(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Border(top: value, bottom: value);
}
///
/// Sets a border on the left side of its content.
/// Learn more
///
public static IContainer BorderLeft(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Border(left: value);
}
///
/// Sets a border on the right side of its content.
/// Learn more
///
public static IContainer BorderRight(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Border(right: value);
}
///
/// Sets a border on the top side of its content.
/// Learn more
///
public static IContainer BorderTop(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Border(top: value);
}
///
/// Sets a border on the bottom side of its content.
/// Learn more
///
public static IContainer BorderBottom(this IContainer element, float value, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.Border(bottom: value);
}
#endregion
#region Corner Radius
private static IContainer CornerRadius(this IContainer element, float? topLeft = null, float? topRight = null, float? bottomRight = null, float? bottomLeft = null)
{
var styledBox = element as StyledBox ?? new StyledBox();
if (topLeft < 0)
throw new ArgumentOutOfRangeException(nameof(topLeft), "The top-left corner radius cannot be negative.");
if (topRight < 0)
throw new ArgumentOutOfRangeException(nameof(topRight), "The top-right corner radius cannot be negative.");
if (bottomRight < 0)
throw new ArgumentOutOfRangeException(nameof(bottomRight), "The bottom-right corner radius cannot be negative.");
if (bottomLeft < 0)
throw new ArgumentOutOfRangeException(nameof(bottomLeft), "The bottom-left corner radius cannot be negative.");
if (topLeft.HasValue)
styledBox.BorderRadiusTopLeft = topLeft.Value;
if (topRight.HasValue)
styledBox.BorderRadiusTopRight = topRight.Value;
if (bottomRight.HasValue)
styledBox.BorderRadiusBottomRight = bottomRight.Value;
if (bottomLeft.HasValue)
styledBox.BorderRadiusBottomLeft = bottomLeft.Value;
return element.Element(styledBox);
}
///
/// Applies a uniform corner radius to all corners of the container with the specified value and unit.
///
public static IContainer CornerRadius(this IContainer element, float value = 0, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.CornerRadius(topLeft: value, topRight: value, bottomRight: value, bottomLeft: value);
}
///
/// Applies a corner radius to the top-left corner of the container with the specified value and unit.
///
public static IContainer CornerRadiusTopLeft(this IContainer element, float value = 0, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.CornerRadius(topLeft: value);
}
///
/// Applies a corner radius to the top-right corner of the container with the specified value and unit.
///
public static IContainer CornerRadiusTopRight(this IContainer element, float value = 0, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.CornerRadius(topRight: value);
}
///
/// Applies a border radius to the bottom-left corner of the container with the specified value and unit.
///
public static IContainer CornerRadiusBottomLeft(this IContainer element, float value = 0, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.CornerRadius(bottomLeft: value);
}
///
/// Applies a corner radius to the bottom-right corner of the container with the specified value and unit.
///
public static IContainer CornerRadiusBottomRight(this IContainer element, float value = 0, Unit unit = Unit.Point)
{
value = value.ToPoints(unit);
return element.CornerRadius(bottomRight: value);
}
#endregion
#region Border Style
///
/// Adjusts color of the border element.
/// Learn more
///
///
public static IContainer BorderColor(this IContainer element, Color color)
{
var border = element as StyledBox ?? new StyledBox();
border.BorderColor = color;
return element.Element(border);
}
///
/// Sets a linear gradient for the border with the specified angle and colors.
///
/// The angle of the gradient in degrees.
/// An array of objects representing the gradient colors.
public static IContainer BorderLinearGradient(this IContainer element, float angle, Color[] colors)
{
if (colors == null || colors.Length == 0)
throw new ArgumentException("The border linear-gradient colors cannot be empty.", nameof(colors));
var border = element as StyledBox ?? new StyledBox();
border.BorderGradientAngle = angle;
border.BorderGradientColors = colors;
return element.Element(border);
}
#endregion
#region Alignment
private static IContainer BorderAlignment(this IContainer element, float value)
{
var border = element as StyledBox ?? new StyledBox();
border.BorderAlignment = value;
return element.Element(border);
}
///
/// Aligns the container's border to the outer edge of the element.
///
public static IContainer BorderAlignmentOutside(this IContainer element)
{
return element.BorderAlignment(1);
}
///
/// Aligns the border in the middle of the specified container boundaries.
/// This option is used by default when no alignment is specified.
///
public static IContainer BorderAlignmentMiddle(this IContainer element)
{
return element.BorderAlignment(0.5f);
}
///
/// Aligns the border to the inside of the container.
///
public static IContainer BorderAlignmentInside(this IContainer element)
{
return element.BorderAlignment(0);
}
#endregion
#region Shadow
///
/// Applies a shadow to the container using the specified shadow style.
/// Shadows can enhance the visual depth and separation of elements in a document.
///
public static IContainer Shadow(this IContainer element, BoxShadowStyle style)
{
if (style == null)
throw new ArgumentNullException(nameof(style), "The box shadow style cannot be null.");
style.Validate();
var styledBox = element as StyledBox ?? new StyledBox();
styledBox.Shadow = style;
return element.Element(styledBox);
}
#endregion
}
}
================================================
FILE: Source/QuestPDF/Fluent/SvgExtensions.cs
================================================
using System;
using System.IO;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SvgImage = QuestPDF.Infrastructure.SvgImage;
namespace QuestPDF.Fluent;
///
/// Generates an SVG image based on the given resolution.
///
/// Desired resolution of the image in pixels.
/// An SVG format compatible text.
public delegate string? GenerateDynamicSvgDelegate(Size size);
public sealed class SvgImageDescriptor
{
private Elements.SvgImage ImageElement { get; }
private AspectRatio AspectRatioElement { get; }
private float ImageAspectRatio { get; }
internal SvgImageDescriptor(Elements.SvgImage imageElement, Elements.AspectRatio aspectRatioElement)
{
ImageElement = imageElement;
AspectRatioElement = aspectRatioElement;
ImageAspectRatio = imageElement.Image.SkSvgImage.AspectRatio;
}
///
/// Scales the image to fill the full width of its container. This is the default behavior.
///
public SvgImageDescriptor FitWidth()
{
return SetAspectRatio(AspectRatioOption.FitWidth);
}
///
/// The image stretches vertically to fit the full available height.
/// Often used with height-constraining elements such as: Height, MaxHeight, etc.
///
public SvgImageDescriptor FitHeight()
{
return SetAspectRatio(AspectRatioOption.FitHeight);
}
///
/// Combines the FitWidth and FitHeight settings.
/// The image resizes itself to utilize all available space, preserving its aspect ratio.
/// It will either fill the width or height based on the container's dimensions.
///
///
/// An optimal and safe choice.
///
public SvgImageDescriptor FitArea()
{
return SetAspectRatio(AspectRatioOption.FitArea);
}
internal SvgImageDescriptor SetAspectRatio(AspectRatioOption option)
{
AspectRatioElement.Ratio = ImageAspectRatio;
AspectRatioElement.Option = option;
return this;
}
}
public static class SvgExtensions
{
internal static void SvgPath(this IContainer container, string svgPath, Color color)
{
container.Element(new SvgPath
{
Path = svgPath,
FillColor = color
});
}
///
/// Draws the SVG image loaded from a text.
/// Learn more
///
///
///
/// Either a path to the SVG file or the SVG content itself.
///
///
public static SvgImageDescriptor Svg(this IContainer container, string svg)
{
var isFile = Path.GetExtension(svg).Equals(".svg", StringComparison.OrdinalIgnoreCase);
var image = isFile ? SvgImage.FromFile(svg) : SvgImage.FromText(svg);
image.IsShared = false;
return container.Svg(image);
}
///
/// Draws the object. Allows to optimize the generation process.
/// Learn more
///
///
///
public static SvgImageDescriptor Svg(this IContainer parent, SvgImage image)
{
var imageElement = new QuestPDF.Elements.SvgImage
{
Image = image
};
var aspectRationElement = new AspectRatio
{
Child = imageElement
};
parent.Element(aspectRationElement);
var bestScalingOption = ImageExtensions.GetBestAspectRatioOptionFromParent(parent);
return new SvgImageDescriptor(imageElement, aspectRationElement).SetAspectRatio(bestScalingOption);
}
///
/// Renders an SVG image of dynamic size dictated by the document layout constraints.
///
///
/// Ideal for integrating with other libraries, e.g. SkiaSharp or ScottPlot.
///
///
/// A delegate that requests an image of desired size.
///
public static void Svg(this IContainer element, GenerateDynamicSvgDelegate dynamicSvgSource)
{
var dynamicImage = new DynamicSvgImage
{
SvgSource = dynamicSvgSource
};
element.Element(dynamicImage);
}
}
================================================
FILE: Source/QuestPDF/Fluent/TableExtensions.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Elements.Table;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public sealed class TableColumnsDefinitionDescriptor
{
internal List Columns { get; } = new();
internal TableColumnsDefinitionDescriptor()
{
}
///
/// Defines a column of constant size that occupies the specified horizontal space.
///
/// The container of the newly created column.
public void ConstantColumn(float width, Unit unit = Unit.Point)
{
if (width <= 0)
throw new ArgumentOutOfRangeException(nameof(width), "Constant column width must be greater than zero.");
ComplexColumn(constantWidth: width.ToPoints(unit));
}
///
/// Defines a column with a relative size that adjusts its width in relation to other relative columns.
/// Learn more
///
///
/// For a table 100 points wide with three columns: a relative size of 1, a relative size of 5, and a constant size of 10 points, they will span 15 points, 75 points, and 10 points respectively.
///
/// The container for the newly defined column.
public void RelativeColumn(float width = 1)
{
if (width <= 0)
throw new ArgumentOutOfRangeException(nameof(width), "Relative column width must be greater than zero.");
ComplexColumn(relativeWidth: width);
}
private void ComplexColumn(float constantWidth = 0, float relativeWidth = 0)
{
var columnDefinition = new TableColumnDefinition(constantWidth, relativeWidth);
Columns.Add(columnDefinition);
}
}
public sealed class TableCellDescriptor
{
private ICollection Cells { get; }
internal TableCellDescriptor(ICollection cells)
{
Cells = cells;
}
///
/// Inserts a new item into the table element.
///
/// The container for the newly inserted cell. Provides options to adjust the cell's position, size, and content.
public ITableCellContainer Cell()
{
var cell = new TableCell();
Cells.Add(cell);
return cell;
}
}
public sealed class TableDescriptor
{
internal bool EnableAutomatedSemanticTagging { get; set; } = false;
private Table HeaderTable { get; } = new();
private Table ContentTable { get; } = new();
private Table FooterTable { get; } = new();
internal TableDescriptor()
{
}
///
/// Specifies the order and size of the table columns.
/// Learn more
///
///
/// This configuration affects both the main content as well as the header and footer sections.
///
/// Handler to define columns of the table.
public void ColumnsDefinition(Action handler)
{
if (ContentTable.Columns.Any())
throw new DocumentComposeException("Table columns have already been defined. Please call the 'Table.ColumnsDefinition' method only once.");
var descriptor = new TableColumnsDefinitionDescriptor();
handler(descriptor);
HeaderTable.Columns = descriptor.Columns;
ContentTable.Columns = descriptor.Columns;
FooterTable.Columns = descriptor.Columns;
}
///
/// Adjusts rendering algorithm to better handle complex table structures, especially those spanning multiple pages.
/// This applies a unique rule to the final cells in each column, ensuring they stretch to fill the table's bottom edge.
/// Such an approach can enhance your table's visual appeal.
///
public void ExtendLastCellsToTableBottom()
{
ContentTable.ExtendLastCellsToTableBottom = true;
}
///
/// Specifies a table header that appears on each page, positioned above the main content.
/// The cell placement and dimensions in this header are distinct from those in the main content.
/// Learn more
///
/// Handler for configuring the header cells.
public void Header(Action handler)
{
if (HeaderTable.Cells.Any())
throw new DocumentComposeException("The 'Table.Header' layer has already been defined. Please call this method only once.");
var descriptor = new TableCellDescriptor(HeaderTable.Cells);
handler(descriptor);
}
///
/// Specifies a table footer that appears on each page, positioned below the main content.
/// The placement and dimensions of cells within this footer are distinct from the main content.
/// Learn more
///
public void Footer(Action handler)
{
if (FooterTable.Cells.Any())
throw new DocumentComposeException("The 'Table.Footer' layer has already been defined. Please call this method only once.");
var descriptor = new TableCellDescriptor(FooterTable.Cells);
handler(descriptor);
}
///
/// Inserts a new item into the table element.
///
/// The container for the newly inserted cell, enabling customization of its position, size, and content.
public ITableCellContainer Cell()
{
var cell = new TableCell();
ContentTable.Cells.Add(cell);
return cell;
}
internal IElement CreateElement()
{
var container = new Container();
var hasHeader = HeaderTable.Cells.Any();
var hasFooter = FooterTable.Cells.Any();
ConfigureTable(HeaderTable, Table.TablePartType.Header);
ConfigureTable(ContentTable, Table.TablePartType.Body);
ConfigureTable(FooterTable, Table.TablePartType.Footer);
var tableRequiresAdvancedHeaderTagging = Table.DoesTableBodyRequireExtendedHeaderTagging(HeaderTable.Cells, ContentTable.Cells);
HeaderTable.TableRequiresAdvancedHeaderTagging = tableRequiresAdvancedHeaderTagging;
ContentTable.TableRequiresAdvancedHeaderTagging = tableRequiresAdvancedHeaderTagging;
ContentTable.HeaderCells = HeaderTable.Cells;
container
.Decoration(decoration =>
{
decoration
.Before()
.ShowIf(hasHeader)
.NonTrackingElement(x => EnableAutomatedSemanticTagging ? x.SemanticTag("THead") : x)
.Element(HeaderTable);
decoration
.Content()
.NonTrackingElement(x => EnableAutomatedSemanticTagging ? x.SemanticTag("TBody") : x)
.ShowIf(ContentTable.Cells.Any())
.Element(ContentTable);
decoration
.After()
.ShowIf(hasFooter)
.NonTrackingElement(x => EnableAutomatedSemanticTagging ? x.SemanticTag("TFoot") : x)
.Element(FooterTable);
});
return container;
void ConfigureTable(Table table, Table.TablePartType tablePartType)
{
if (!table.Columns.Any())
throw new DocumentComposeException($"Table should have at least one column. Please call the '{nameof(ColumnsDefinition)}' method to define columns.");
table.PlanCellPositions();
table.ValidateCellPositions();
table.EnableAutomatedSemanticTagging = EnableAutomatedSemanticTagging;
table.PartType = tablePartType;
}
}
}
public static class TableExtensions
{
///
/// Renders a set of items utilizing the table layout algorithm.
///
/// Items may be auto-placed based on the order they're called or can have assigned specific column and row positions. Cells can also span multiple columns and/or rows.
///
///
/// Handler to define the table content.
public static void Table(this IContainer element, Action handler)
{
var descriptor = new TableDescriptor();
descriptor.EnableAutomatedSemanticTagging = element is SemanticTag { TagType: "Table" };
handler(descriptor);
element.Element(descriptor.CreateElement());
}
}
public static class TableCellExtensions
{
///
/// Specifies the column position (horizontal axis) of the cell.
/// Learn more
///
/// Columns are numbered starting with 1.
public static ITableCellContainer Column(this ITableCellContainer tableCellContainer, uint value)
{
if (tableCellContainer is TableCell tableCell)
tableCell.Column = (int)value;
return tableCellContainer;
}
///
/// Defines the number of columns a cell spans in the horizontal axis.
/// Learn more
///
///
/// Useful when creating complex layouts.
///
public static ITableCellContainer ColumnSpan(this ITableCellContainer tableCellContainer, uint value)
{
if (tableCellContainer is TableCell tableCell)
tableCell.ColumnSpan = (int)value;
return tableCellContainer;
}
///
/// Specifies the row position (vertical axis) of the cell.
/// Learn more
///
/// Rows are numbered starting with 1.
public static ITableCellContainer Row(this ITableCellContainer tableCellContainer, uint value)
{
if (tableCellContainer is TableCell tableCell)
tableCell.Row = (int)value;
return tableCellContainer;
}
///
/// Defines the number of rows a cell spans in the vertical axis.
/// Learn more
///
///
/// Useful when creating complex layouts.
///
public static ITableCellContainer RowSpan(this ITableCellContainer tableCellContainer, uint value)
{
if (tableCellContainer is TableCell tableCell)
tableCell.RowSpan = (int)value;
return tableCellContainer;
}
///
/// Marks the specified table cell as a semantic horizontal header.
/// This allows assistive technologies to recognize the cell as a header, improving accessibility and semantic structure.
///
public static ITableCellContainer AsSemanticHorizontalHeader(this ITableCellContainer tableCellContainer)
{
if (tableCellContainer is TableCell tableCell)
tableCell.IsSemanticHorizontalHeader = true;
return tableCellContainer;
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/TextExtensions.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Elements.Text;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class TextSpanDescriptor
{
internal readonly TextBlockSpan TextBlockSpan;
internal TextSpanDescriptor(TextBlockSpan textBlockSpan)
{
TextBlockSpan = textBlockSpan;
}
internal void MutateTextStyle(Func handler, T argument)
{
TextBlockSpan.Style = handler(TextBlockSpan.Style, argument);
}
internal void MutateTextStyle(Func handler)
{
TextBlockSpan.Style = handler(TextBlockSpan.Style);
}
}
///
/// Transforms a page number into a custom text format (e.g., roman numerals).
///
///
/// When is null, the delegate should return a default placeholder text of a typical length.
///
public delegate string PageNumberFormatter(int? pageNumber);
public sealed class TextPageNumberDescriptor : TextSpanDescriptor
{
internal Action AssignFormatFunction { get; }
internal TextPageNumberDescriptor(TextBlockSpan textBlockSpan, Action assignFormatFunction) : base(textBlockSpan)
{
AssignFormatFunction = assignFormatFunction;
AssignFormatFunction(x => x?.ToString());
}
///
/// Provides the capability to render the page number in a custom text format (e.g., roman numerals).
/// Lear more
///
/// The function designated to modify the number into text. When given a null input, a typical-sized placeholder text must be produced.
public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
{
AssignFormatFunction(formatter);
return this;
}
}
public sealed class TextBlockDescriptor : TextSpanDescriptor
{
private TextBlock TextBlock;
internal TextBlockDescriptor(TextBlock textBlock, TextBlockSpan textBlockSpan) : base(textBlockSpan)
{
TextBlock = textBlock;
}
///
public TextBlockDescriptor AlignLeft()
{
TextBlock.Alignment = TextHorizontalAlignment.Left;
return this;
}
///
public TextBlockDescriptor AlignCenter()
{
TextBlock.Alignment = TextHorizontalAlignment.Center;
return this;
}
///
public TextBlockDescriptor AlignRight()
{
TextBlock.Alignment = TextHorizontalAlignment.Right;
return this;
}
///
public TextBlockDescriptor Justify()
{
TextBlock.Alignment = TextHorizontalAlignment.Justify;
return this;
}
///
public TextBlockDescriptor AlignStart()
{
TextBlock.Alignment = TextHorizontalAlignment.Start;
return this;
}
///
public TextBlockDescriptor AlignEnd()
{
TextBlock.Alignment = TextHorizontalAlignment.End;
return this;
}
///
public TextBlockDescriptor ClampLines(int maxLines, string ellipsis = TextDescriptor.DefaultLineClampEllipsis)
{
if (maxLines < 0)
throw new ArgumentException("Line clamp must be greater or equal to zero", nameof(maxLines));
TextBlock.LineClamp = maxLines;
TextBlock.LineClampEllipsis = ellipsis ?? TextDescriptor.DefaultLineClampEllipsis;
return this;
}
///
public TextBlockDescriptor ParagraphSpacing(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentException("Paragraph spacing must be greater or equal to zero", nameof(value));
TextBlock.ParagraphSpacing = value.ToPoints(unit);
return this;
}
///
public TextBlockDescriptor ParagraphFirstLineIndentation(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentException("Paragraph indentation must be greater or equal to zero", nameof(value));
TextBlock.ParagraphFirstLineIndentation = value.ToPoints(unit);
return this;
}
}
public sealed class TextDescriptor
{
internal TextBlock TextBlock { get; } = new();
private TextStyle? DefaultStyle { get; set; }
internal const string DefaultLineClampEllipsis = "…";
internal TextDescriptor()
{
}
///
/// Applies a consistent text style for the whole content within this Text element.
///
/// The TextStyle object to override the default inherited text style.
public void DefaultTextStyle(TextStyle style)
{
DefaultStyle = style;
}
///
/// Applies a consistent text style for the whole content within this Text element.
///
/// Handler to modify the default inherited text style.
public void DefaultTextStyle(Func style)
{
DefaultStyle = style(TextStyle.Default);
}
///
public void AlignLeft()
{
TextBlock.Alignment = TextHorizontalAlignment.Left;
}
///
public void AlignCenter()
{
TextBlock.Alignment = TextHorizontalAlignment.Center;
}
///
public void AlignRight()
{
TextBlock.Alignment = TextHorizontalAlignment.Right;
}
///
public void Justify()
{
TextBlock.Alignment = TextHorizontalAlignment.Justify;
}
///
public void AlignStart()
{
TextBlock.Alignment = TextHorizontalAlignment.Start;
}
///
public void AlignEnd()
{
TextBlock.Alignment = TextHorizontalAlignment.End;
}
///
public void ClampLines(int maxLines, string ellipsis = DefaultLineClampEllipsis)
{
TextBlock.LineClamp = maxLines;
TextBlock.LineClampEllipsis = ellipsis;
}
///
public void ParagraphSpacing(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentException("Paragraph spacing must be greater or equal to zero", nameof(value));
TextBlock.ParagraphSpacing = value.ToPoints(unit);
}
///
public void ParagraphFirstLineIndentation(float value, Unit unit = Unit.Point)
{
if (value < 0)
throw new ArgumentException("Paragraph indentation must be greater or equal to zero", nameof(value));
TextBlock.ParagraphFirstLineIndentation = value.ToPoints(unit);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
[ExcludeFromCodeCoverage]
public void Span(string? text, TextStyle style)
{
Span(text).Style(style);
}
///
/// Appends the given text to the current paragraph.
///
///
public TextSpanDescriptor Span(string? text)
{
if (text == null)
return new TextSpanDescriptor(new TextBlockSpan());
var textSpan = new TextBlockSpan() { Text = text };
TextBlock.Items.Add(textSpan);
return new TextSpanDescriptor(textSpan);
}
///
/// Appends a line with the provided text followed by an environment-specific newline character.
///
///
public TextSpanDescriptor Line(string? text)
{
text ??= string.Empty;
return Span(text + "\n");
}
///
/// Appends a blank line.
///
///
public TextSpanDescriptor EmptyLine()
{
return Span("\n");
}
private TextPageNumberDescriptor PageNumber(Func pageNumber)
{
var textBlockItem = new TextBlockPageNumber();
TextBlock.Items.Add(textBlockItem);
return new TextPageNumberDescriptor(textBlockItem, x => textBlockItem.Source = context => x(pageNumber(context)));
}
///
/// Appends text showing the current page number.
///
///
public TextPageNumberDescriptor CurrentPageNumber()
{
return PageNumber(x => x.CurrentPage);
}
///
/// Appends text showing the total number of pages in the document.
///
///
public TextPageNumberDescriptor TotalPages()
{
return PageNumber(x => x.DocumentLength);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the BeginPageNumberOfSection method.")]
[ExcludeFromCodeCoverage]
public void PageNumberOfLocation(string sectionName, TextStyle? style = null)
{
BeginPageNumberOfSection(sectionName).Style(style);
}
///
/// Appends text showing the number of the first page of the specified Section.
///
///
///
public TextPageNumberDescriptor BeginPageNumberOfSection(string sectionName)
{
return PageNumber(x => x.GetLocation(sectionName)?.PageStart);
}
///
/// Appends text showing the number of the last page of the specified Section.
///
///
///
public TextPageNumberDescriptor EndPageNumberOfSection(string sectionName)
{
return PageNumber(x => x.GetLocation(sectionName)?.PageEnd);
}
///
/// Appends text showing the page number relative to the beginning of the given Section.
///
///
/// For a section spanning pages 20 to 50, page 35 will show as 15.
///
///
///
public TextPageNumberDescriptor PageNumberWithinSection(string sectionName)
{
return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(sectionName)?.PageStart);
}
///
/// Appends text showing the total number of pages within the given Section.
///
///
/// For a section spanning pages 20 to 50, the total is 30 pages.
///
///
///
public TextPageNumberDescriptor TotalPagesWithinSection(string sectionName)
{
return PageNumber(x => x.GetLocation(sectionName)?.Length);
}
///
/// Creates a clickable text that navigates the user to a specified Section.
///
///
///
public TextSpanDescriptor SectionLink(string? text, string sectionName)
{
if (string.IsNullOrWhiteSpace(sectionName))
throw new ArgumentException("Section name cannot be null or whitespace.", nameof(sectionName));
if (text == null)
return new TextSpanDescriptor(new TextBlockSpan());
var textBlockItem = new TextBlockSectionLink
{
Text = text,
SectionName = sectionName
};
TextBlock.Items.Add(textBlockItem);
return new TextSpanDescriptor(textBlockItem);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
[ExcludeFromCodeCoverage]
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
{
SectionLink(text, locationName).Style(style);
}
///
/// Creates a clickable text that redirects the user to a specific webpage.
///
///
///
public TextSpanDescriptor Hyperlink(string? text, string url)
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("Url cannot be null or whitespace.", nameof(url));
if (text == null)
return new TextSpanDescriptor(new TextBlockSpan());
var textBlockItem = new TextBlockHyperlink
{
Text = text,
Url = url
};
TextBlock.Items.Add(textBlockItem);
return new TextSpanDescriptor(textBlockItem);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
[ExcludeFromCodeCoverage]
public void ExternalLocation(string? text, string url, TextStyle? style = null)
{
Hyperlink(text, url).Style(style);
}
///
/// Embeds custom content within the text.
///
///
/// The container must fit within one line and can not span multiple pages.
///
/// Defines the position of the injected element in relation to text typography features (baseline, top/bottom edge).
/// A container for the embedded content. Populate using the Fluent API.
public IContainer Element(TextInjectedElementAlignment alignment = TextInjectedElementAlignment.AboveBaseline)
{
var container = new Container();
TextBlock.Items.Add(new TextBlockElement
{
Element = container,
Alignment = alignment
});
return container.AlignBottom().MinimalBox();
}
///
/// Embeds custom content within the text.
///
///
/// The container must fit within one line and can not span multiple pages.
///
/// Defines the position of the injected element in relation to text typography features (baseline, top/bottom edge).
/// Delegate to populate the embedded container with custom content.
public void Element(Action handler, TextInjectedElementAlignment alignment = TextInjectedElementAlignment.AboveBaseline)
{
handler(Element(alignment));
}
internal void Compose(IContainer container)
{
if (DefaultStyle != null)
container = container.DefaultTextStyle(DefaultStyle);
container.Element(TextBlock);
}
}
public static class TextExtensions
{
///
/// Draws rich formatted text.
///
/// Handler to define the content of the text elements (e.g.: paragraphs, spans, hyperlinks, page numbers).
public static void Text(this IContainer element, Action content)
{
var descriptor = new TextDescriptor();
if (element is Alignment alignment)
descriptor.TextBlock.Alignment = MapAlignment(alignment.Horizontal);
content?.Invoke(descriptor);
descriptor.Compose(element);
}
[Obsolete("This method has been deprecated since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
[ExcludeFromCodeCoverage]
public static void Text(this IContainer element, object? text, TextStyle style)
{
element.Text(text).Style(style);
}
[Obsolete("This method has been deprecated since version 2022.12. Please use an overload where the text parameter is passed explicitly as a string.")]
[ExcludeFromCodeCoverage]
public static TextSpanDescriptor Text(this IContainer element, object? text)
{
return element.Text(text?.ToString());
}
///
/// Draws the provided text on the page
///
///
public static TextBlockDescriptor Text(this IContainer container, string? text)
{
if (text == null)
return new TextBlockDescriptor(new TextBlock(), new TextBlockSpan());
var textBlock = new TextBlock();
container.Element(textBlock);
if (container is Alignment alignment)
textBlock.Alignment = MapAlignment(alignment.Horizontal);
var textSpan = new TextBlockSpan { Text = text };
textBlock.Items.Add(textSpan);
return new TextBlockDescriptor(textBlock, textSpan);
}
private static TextHorizontalAlignment? MapAlignment(HorizontalAlignment? alignment)
{
return alignment switch
{
HorizontalAlignment.Left => TextHorizontalAlignment.Left,
HorizontalAlignment.Center => TextHorizontalAlignment.Center,
HorizontalAlignment.Right => TextHorizontalAlignment.Right,
_ => null
};
}
}
}
================================================
FILE: Source/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class TextSpanDescriptorExtensions
{
///
public static T Style(this T descriptor, TextStyle style) where T : TextSpanDescriptor
{
if (style == null)
return descriptor;
descriptor.MutateTextStyle(TextStyleManager.OverrideStyle, style);
return descriptor;
}
[Obsolete("This setting is not supported since the 2024.3 version. Please use the FontFamilyFallback method or rely on the new automated fallback mechanism.")]
[ExcludeFromCodeCoverage]
public static T Fallback(this T descriptor, TextStyle? value = null) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.Fallback, value);
return descriptor;
}
[Obsolete("This setting is not supported since the 2024.3 version. Please use the FontFamilyFallback method or rely on the new automated fallback mechanism.")]
[ExcludeFromCodeCoverage]
public static T Fallback(this T descriptor, Func handler) where T : TextSpanDescriptor
{
return descriptor.Fallback(handler(TextStyle.Default));
}
///
///
public static T FontColor(this T descriptor, Color color) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.FontColor, color);
return descriptor;
}
///
///
public static T BackgroundColor(this T descriptor, Color color) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.BackgroundColor, color);
return descriptor;
}
///
public static T FontFamily(this T descriptor, params string[] values) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.FontFamily, values);
return descriptor;
}
///
public static T FontSize(this T descriptor, float value) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.FontSize, value);
return descriptor;
}
///
public static T LineHeight(this T descriptor, float? factor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.LineHeight, factor);
return descriptor;
}
///
public static T LetterSpacing(this T descriptor, float factor = 0) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.LetterSpacing, factor);
return descriptor;
}
///
public static T WordSpacing(this T descriptor, float factor = 0) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.WordSpacing, factor);
return descriptor;
}
///
public static T Italic(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.Italic, value);
return descriptor;
}
///
[Obsolete("This setting is not supported since the 2024.3 version. This flag should be handled automatically by the layout engine.")]
[ExcludeFromCodeCoverage]
public static T WrapAnywhere(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
return descriptor;
}
#region Text Effects
///
///
public static T Strikethrough(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.Strikethrough, value);
return descriptor;
}
///
///
public static T Underline(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.Underline, value);
return descriptor;
}
///
///
public static T Overline(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.Overline, value);
return descriptor;
}
///
///
public static T DecorationColor(this T descriptor, Color color) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.DecorationColor, color);
return descriptor;
}
///
///
public static T DecorationThickness(this T descriptor, float factor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.DecorationThickness, factor);
return descriptor;
}
///
///
public static T DecorationSolid(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.DecorationSolid);
return descriptor;
}
///
///
public static T DecorationDouble(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.DecorationDouble);
return descriptor;
}
///
///
public static T DecorationWavy(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.DecorationWavy);
return descriptor;
}
///
///
public static T DecorationDotted(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.DecorationDotted);
return descriptor;
}
///
///
public static T DecorationDashed(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.DecorationDashed);
return descriptor;
}
#endregion
#region Weight
///
///
public static T Thin(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.Thin);
return descriptor;
}
///
///
public static T ExtraLight(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.ExtraLight);
return descriptor;
}
///
///
public static T Light(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.Light);
return descriptor;
}
///
///
public static T NormalWeight(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.NormalWeight);
return descriptor;
}
///
///
public static T Medium(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.Medium);
return descriptor;
}
///
///
public static T SemiBold(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(TextStyleExtensions.SemiBold);
return descriptor;
}
///