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. [![GitHub Stars and Stargazers](https://img.shields.io/github/stars/QuestPDF/QuestPDF?style=for-the-badge&label=GitHub%20Stars&logo=github&color=FFEB3B&logoColor=white)](https://github.com/QuestPDF/QuestPDF)
[![Nuget package download](https://img.shields.io/nuget/dt/QuestPDF?style=for-the-badge&label=NuGet%20downloads&logo=nuget&color=0277BD&logoColor=white)](https://www.nuget.org/packages/QuestPDF/)
[![QuestPDF License](https://img.shields.io/badge/LICENSE-Community%20and%20commercial-2E7D32?style=for-the-badge&logo=googledocs&logoColor=white)](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: image [![Quick Start Tutorial](https://img.shields.io/badge/read-tutorial-0288D1?style=for-the-badge)](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, [![Explore All QuestPDF Features](https://img.shields.io/badge/explore%20all%20features-0288D1?style=for-the-badge)](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 [![Learn about QuestPDF Companion App](https://img.shields.io/badge/learn%20more-0288D1?style=for-the-badge)]([https://www.questpdf.com/companion/features.html](https://www.questpdf.com/companion/usage.html)) [![Learn about QuestPDF Companion App](https://img.shields.io/badge/features-666666?style=for-the-badge)](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"); ``` [![Learn Document Operation API](https://img.shields.io/badge/learn%20more-0288D1?style=for-the-badge)](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. [![QuestPDF Pricing](https://img.shields.io/badge/view%20pricing-388E3C?style=for-the-badge)](https://www.questpdf.com/license.html) [![QuestPDF License Terms](https://img.shields.io/badge/license%20terms-666666?style=for-the-badge)](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 Example Invoice [![Read Real-world Invoice Tutorial](https://img.shields.io/badge/read%20tutorial-0288D1?style=for-the-badge)](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 [![Nick Chapsas The Easiest Way to Create PDFs in .NET](https://github.com/user-attachments/assets/5c7fc84b-65d6-4ec2-9cc2-b2acbc9764d0)](https://www.youtube.com/watch?v=_M0IgtGWnvE) ### JetBrains: OSS Power-Ups: QuestPDF [![JetBrains OSS Power-Ups: QuestPDF](https://github.com/user-attachments/assets/3519b532-c2aa-430e-ab1b-f40edd3fa120)](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; } /// /// public static T Bold(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.Bold); return descriptor; } /// /// public static T ExtraBold(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.ExtraBold); return descriptor; } /// /// public static T Black(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.Black); return descriptor; } /// /// public static T ExtraBlack(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.ExtraBlack); return descriptor; } #endregion #region Position /// public static T NormalPosition(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.NormalPosition); return descriptor; } /// public static T Subscript(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.Subscript); return descriptor; } /// public static T Superscript(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.Superscript); return descriptor; } #endregion #region Direction /// public static T DirectionAuto(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.DirectionAuto); return descriptor; } /// public static T DirectionFromLeftToRight(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.DirectionFromLeftToRight); return descriptor; } /// public static T DirectionFromRightToLeft(this T descriptor) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.DirectionFromRightToLeft); return descriptor; } #endregion #region Font Features /// public static T EnableFontFeature(this T descriptor, string featureName) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.EnableFontFeature, featureName); return descriptor; } /// public static T DisableFontFeature(this T descriptor, string featureName) where T : TextSpanDescriptor { descriptor.MutateTextStyle(TextStyleExtensions.DisableFontFeature, featureName); return descriptor; } #endregion } } ================================================ FILE: Source/QuestPDF/Fluent/TextStyleExtensions.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using QuestPDF.Skia.Text; namespace QuestPDF.Fluent { public static class TextStyleExtensions { [Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")] [ExcludeFromCodeCoverage] public static TextStyle Color(this TextStyle style, string value) { return style.FontColor(value); } /// /// public static TextStyle FontColor(this TextStyle style, Color color) { return style .Mutate(TextStyleProperty.Color, color) .Mutate(TextStyleProperty.DecorationColor, color); } /// /// public static TextStyle BackgroundColor(this TextStyle style, Color color) { return style.Mutate(TextStyleProperty.BackgroundColor, color); } [Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")] [ExcludeFromCodeCoverage] public static TextStyle FontType(this TextStyle style, string value) { return style.FontFamily(value); } /// public static TextStyle FontFamily(this TextStyle style, params string[] values) { if (values == null || values.Length == 0) return style; return style.Mutate(TextStyleProperty.FontFamilies, values); } [Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")] [ExcludeFromCodeCoverage] public static TextStyle Size(this TextStyle style, float value) { return style.FontSize(value); } /// public static TextStyle FontSize(this TextStyle style, float value) { if (value <= 0) throw new ArgumentException("Font size must be greater than 0."); return style.Mutate(TextStyleProperty.Size, value); } /// public static TextStyle LineHeight(this TextStyle style, float? factor) { factor ??= TextStyle.NormalLineHeightCalculatedFromFontMetrics; if (factor < 0) throw new ArgumentException("Line height must be greater than 0."); return style.Mutate(TextStyleProperty.LineHeight, factor); } /// public static TextStyle LetterSpacing(this TextStyle style, float factor = 0) { return style.Mutate(TextStyleProperty.LetterSpacing, factor); } /// public static TextStyle WordSpacing(this TextStyle style, float factor = 0) { return style.Mutate(TextStyleProperty.WordSpacing, factor); } /// public static TextStyle Italic(this TextStyle style, bool value = true) { return style.Mutate(TextStyleProperty.IsItalic, value); } /// [Obsolete("This setting is not supported since the 2024.3 version. This flag should be handled automatically by the layout engine.")] [ExcludeFromCodeCoverage] public static TextStyle WrapAnywhere(this TextStyle style, bool value = true) { return style; } #region Text Effects /// /// public static TextStyle Strikethrough(this TextStyle style, bool value = true) { return style.Mutate(TextStyleProperty.HasStrikethrough, value); } /// /// public static TextStyle Underline(this TextStyle style, bool value = true) { return style.Mutate(TextStyleProperty.HasUnderline, value); } /// /// public static TextStyle Overline(this TextStyle style, bool value = true) { return style.Mutate(TextStyleProperty.HasOverline, value); } /// /// public static TextStyle DecorationColor(this TextStyle style, Color color) { return style.Mutate(TextStyleProperty.DecorationColor, color); } /// /// public static TextStyle DecorationThickness(this TextStyle style, float factor) { return style.Mutate(TextStyleProperty.DecorationThickness, factor); } /// /// public static TextStyle DecorationSolid(this TextStyle style) { return style.Mutate(TextStyleProperty.DecorationStyle, TextStyleConfiguration.TextDecorationStyle.Solid); } /// /// public static TextStyle DecorationDouble(this TextStyle style) { return style.Mutate(TextStyleProperty.DecorationStyle, TextStyleConfiguration.TextDecorationStyle.Double); } /// /// public static TextStyle DecorationWavy(this TextStyle style) { return style.Mutate(TextStyleProperty.DecorationStyle, TextStyleConfiguration.TextDecorationStyle.Wavy); } /// /// public static TextStyle DecorationDotted(this TextStyle style) { return style.Mutate(TextStyleProperty.DecorationStyle, TextStyleConfiguration.TextDecorationStyle.Dotted); } /// /// public static TextStyle DecorationDashed(this TextStyle style) { return style.Mutate(TextStyleProperty.DecorationStyle, TextStyleConfiguration.TextDecorationStyle.Dashed); } #endregion #region Weight public static TextStyle Weight(this TextStyle style, FontWeight weight) { return style.Mutate(TextStyleProperty.FontWeight, weight); } /// /// public static TextStyle Thin(this TextStyle style) { return style.Weight(FontWeight.Thin); } /// /// public static TextStyle ExtraLight(this TextStyle style) { return style.Weight(FontWeight.ExtraLight); } /// /// public static TextStyle Light(this TextStyle style) { return style.Weight(FontWeight.Light); } /// /// public static TextStyle NormalWeight(this TextStyle style) { return style.Weight(FontWeight.Normal); } /// /// public static TextStyle Medium(this TextStyle style) { return style.Weight(FontWeight.Medium); } /// /// public static TextStyle SemiBold(this TextStyle style) { return style.Weight(FontWeight.SemiBold); } /// /// public static TextStyle Bold(this TextStyle style) { return style.Weight(FontWeight.Bold); } /// /// public static TextStyle ExtraBold(this TextStyle style) { return style.Weight(FontWeight.ExtraBold); } /// /// public static TextStyle Black(this TextStyle style) { return style.Weight(FontWeight.Black); } /// /// public static TextStyle ExtraBlack(this TextStyle style) { return style.Weight(FontWeight.ExtraBlack); } #endregion #region Position /// public static TextStyle NormalPosition(this TextStyle style) { return style.Position(FontPosition.Normal); } /// public static TextStyle Subscript(this TextStyle style) { return style.Position(FontPosition.Subscript); } /// public static TextStyle Superscript(this TextStyle style) { return style.Position(FontPosition.Superscript); } private static TextStyle Position(this TextStyle style, FontPosition fontPosition) { return style.Mutate(TextStyleProperty.FontPosition, fontPosition); } #endregion #region Fallback [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 TextStyle Fallback(this TextStyle style, TextStyle? value = null) { var currentFontFamilies = style.FontFamilies ?? Array.Empty(); var fallbackFontFamilies = value?.FontFamilies ?? Array.Empty(); var targetFontFamilies = currentFontFamilies .Concat(fallbackFontFamilies) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct() .ToArray(); return style.FontFamily(targetFontFamilies); } [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 TextStyle Fallback(this TextStyle style, Func handler) { return style.Fallback(handler(TextStyle.Default)); } #endregion #region Direction private static TextStyle TextDirection(this TextStyle style, TextDirection textDirection) { return style.Mutate(TextStyleProperty.Direction, textDirection); } /// public static TextStyle DirectionAuto(this TextStyle style) { return style.TextDirection(Infrastructure.TextDirection.Auto); } /// public static TextStyle DirectionFromLeftToRight(this TextStyle style) { return style.TextDirection(Infrastructure.TextDirection.LeftToRight); } /// public static TextStyle DirectionFromRightToLeft(this TextStyle style) { return style.TextDirection(Infrastructure.TextDirection.RightToLeft); } #endregion #region Font Features /// public static TextStyle EnableFontFeature(this TextStyle style, string featureName) { return style.Mutate(TextStyleProperty.FontFeatures, new[] { (featureName, true) }); } /// public static TextStyle DisableFontFeature(this TextStyle style, string featureName) { return style.Mutate(TextStyleProperty.FontFeatures, new[] { (featureName, false) }); } #endregion } } ================================================ FILE: Source/QuestPDF/Fluent/TranslateExtensions.cs ================================================ using QuestPDF.Elements; using QuestPDF.Infrastructure; namespace QuestPDF.Fluent { public static class TranslateExtensions { private static IContainer Translate(this IContainer element, float x = 0, float y = 0) { var translate = element as Translate ?? new Translate(); translate.TranslateX += x; translate.TranslateY += y; return element.Element(translate); } /// /// Moves content along the horizontal axis. /// A positive value moves content to the right; a negative value moves it to the left. /// Does not alter the available space. ///
/// Learn more ///
public static IContainer TranslateX(this IContainer element, float value, Unit unit = Unit.Point) { value = value.ToPoints(unit); return element.Translate(x: value); } /// /// Moves content along the vertical axis. /// A positive value moves content downwards, a negative value moves it upwards. /// Does not alter the available space. ///
/// Learn more ///
public static IContainer TranslateY(this IContainer element, float value, Unit unit = Unit.Point) { value = value.ToPoints(unit); return element.Translate(y: value); } } } ================================================ FILE: Source/QuestPDF/Helpers/CallerArgumentExpression.cs ================================================ #if !NET6_0_OR_GREATER namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] internal sealed class CallerArgumentExpressionAttribute : Attribute { public CallerArgumentExpressionAttribute(string parameterName) { ParameterName = parameterName; } public string ParameterName { get; } } } #endif ================================================ FILE: Source/QuestPDF/Helpers/ColorParser.cs ================================================ using System; using System.Collections.Concurrent; using System.Globalization; using QuestPDF.Infrastructure; namespace QuestPDF.Helpers; static class ColorParser { private static readonly ConcurrentDictionary Cache = new(); public static Color ParseColorHex(string hexString) { if (!TryParseColorHex(hexString, out var color)) { throw new ArgumentException( $"The provided value '{hexString}' is not a valid hex color. " + "The following formats are supported: #RGB, #ARGB, #RRGGBB, #AARRGGBB. " + "The hash sign is optional so the following formats are also valid: RGB, ARGB, RRGGBB, AARRGGBB. " + "For example #FF8800 is a solid orange color, while #20CF is a barely visible aqua color.", nameof(color)); } return color; } // inspired by: https://github.com/mono/SkiaSharp/blob/9274aeec807fd17eec2a3266ad4c2475c37d8a0c/binding/SkiaSharp/SKColor.cs#L123 public static bool TryParseColorHex(string hexString, out Color color) { var result = Cache.GetOrAdd(hexString, ParseColor); color = result ?? Colors.Black; return result.HasValue; static Color? ParseColor(string hexString) { uint color = 0; if (string.IsNullOrWhiteSpace(hexString)) return null; // clean up string var hexSpan = hexString.Trim().TrimStart('#'); var len = hexSpan.Length; if (len == 3 || len == 4) { byte a; // parse [A] if (len == 4) { if (!byte.TryParse(hexSpan.Substring(0, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out a)) return null; a = (byte)((a << 4) | a); } else { a = 255; } // parse RGB if (!byte.TryParse(hexSpan.Substring(len - 3, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var r) || !byte.TryParse(hexSpan.Substring(len - 2, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var g) || !byte.TryParse(hexSpan.Substring(len - 1, 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b)) { return null; } r |= (byte)(r << 4); g |= (byte)(g << 4); b |= (byte)(b << 4); // success color = (uint)((a << 24) | (r << 16) | (g << 8) | b); return new Color(color); } if (len == 6 || len == 8) { // parse [AA]RRGGBB if (!uint.TryParse(hexSpan, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var number)) return null; // success color = number; // alpha was not provided, so use 255 if (len == 6) color |= 0xFF000000; return new Color(color); } return null; } } } ================================================ FILE: Source/QuestPDF/Helpers/Colors.cs ================================================ using QuestPDF.Infrastructure; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace QuestPDF.Helpers { /// /// Offers a palette of colors defined by the Google Material Design System guidelines. /// Each primary color (like red, blue, green) has associated shades and accent variations. ///
/// Learn more ///
/// /// Access colors using the following pattern: /// /// Colors.Black; /// Colors.Red.Lighten5; /// Colors.Blue.Medium; /// Colors.Green.Darken4; /// Colors.Yellow.Accent2; /// /// public static class Colors { public static readonly Color Black = new(0xFF000000); public static readonly Color White = new(0xFFFFFFFF); public static readonly Color Transparent = new(0x00000000); public static class Red { public static readonly Color Lighten5 = new(0xFFFFEBEE); public static readonly Color Lighten4 = new(0xFFFFCDD2); public static readonly Color Lighten3 = new(0xFFEF9A9A); public static readonly Color Lighten2 = new(0xFFE57373); public static readonly Color Lighten1 = new(0xFFEF5350); public static readonly Color Medium = new(0xFFF44336); public static readonly Color Darken1 = new(0xFFE53935); public static readonly Color Darken2 = new(0xFFD32F2F); public static readonly Color Darken3 = new(0xFFC62828); public static readonly Color Darken4 = new(0xFFB71C1C); public static readonly Color Accent1 = new(0xFFFF8A80); public static readonly Color Accent2 = new(0xFFFF5252); public static readonly Color Accent3 = new(0xFFFF1744); public static readonly Color Accent4 = new(0xFFD50000); } public static class Pink { public static readonly Color Lighten5 = new(0xFFFCE4EC); public static readonly Color Lighten4 = new(0xFFF8BBD0); public static readonly Color Lighten3 = new(0xFFF48FB1); public static readonly Color Lighten2 = new(0xFFF06292); public static readonly Color Lighten1 = new(0xFFEC407A); public static readonly Color Medium = new(0xFFE91E63); public static readonly Color Darken1 = new(0xFFD81B60); public static readonly Color Darken2 = new(0xFFC2185B); public static readonly Color Darken3 = new(0xFFAD1457); public static readonly Color Darken4 = new(0xFF880E4F); public static readonly Color Accent1 = new(0xFFFF80AB); public static readonly Color Accent2 = new(0xFFFF4081); public static readonly Color Accent3 = new(0xFFF50057); public static readonly Color Accent4 = new(0xFFC51162); } public static class Purple { public static readonly Color Lighten5 = new(0xFFF3E5F5); public static readonly Color Lighten4 = new(0xFFE1BEE7); public static readonly Color Lighten3 = new(0xFFCE93D8); public static readonly Color Lighten2 = new(0xFFBA68C8); public static readonly Color Lighten1 = new(0xFFAB47BC); public static readonly Color Medium = new(0xFF9C27B0); public static readonly Color Darken1 = new(0xFF8E24AA); public static readonly Color Darken2 = new(0xFF7B1FA2); public static readonly Color Darken3 = new(0xFF6A1B9A); public static readonly Color Darken4 = new(0xFF4A148C); public static readonly Color Accent1 = new(0xFFEA80FC); public static readonly Color Accent2 = new(0xFFE040FB); public static readonly Color Accent3 = new(0xFFD500F9); public static readonly Color Accent4 = new(0xFFAA00FF); } public static class DeepPurple { public static readonly Color Lighten5 = new(0xFFEDE7F6); public static readonly Color Lighten4 = new(0xFFD1C4E9); public static readonly Color Lighten3 = new(0xFFB39DDB); public static readonly Color Lighten2 = new(0xFF9575CD); public static readonly Color Lighten1 = new(0xFF7E57C2); public static readonly Color Medium = new(0xFF673AB7); public static readonly Color Darken1 = new(0xFF5E35B1); public static readonly Color Darken2 = new(0xFF512DA8); public static readonly Color Darken3 = new(0xFF4527A0); public static readonly Color Darken4 = new(0xFF311B92); public static readonly Color Accent1 = new(0xFFB388FF); public static readonly Color Accent2 = new(0xFF7C4DFF); public static readonly Color Accent3 = new(0xFF651FFF); public static readonly Color Accent4 = new(0xFF6200EA); } public static class Indigo { public static readonly Color Lighten5 = new(0xFFE8EAF6); public static readonly Color Lighten4 = new(0xFFC5CAE9); public static readonly Color Lighten3 = new(0xFF9FA8DA); public static readonly Color Lighten2 = new(0xFF7986CB); public static readonly Color Lighten1 = new(0xFF5C6BC0); public static readonly Color Medium = new(0xFF3F51B5); public static readonly Color Darken1 = new(0xFF3949AB); public static readonly Color Darken2 = new(0xFF303F9F); public static readonly Color Darken3 = new(0xFF283593); public static readonly Color Darken4 = new(0xFF1A237E); public static readonly Color Accent1 = new(0xFF8C9EFF); public static readonly Color Accent2 = new(0xFF536DFE); public static readonly Color Accent3 = new(0xFF3D5AFE); public static readonly Color Accent4 = new(0xFF304FFE); } public static class Blue { public static readonly Color Lighten5 = new(0xFFE3F2FD); public static readonly Color Lighten4 = new(0xFFBBDEFB); public static readonly Color Lighten3 = new(0xFF90CAF9); public static readonly Color Lighten2 = new(0xFF64B5F6); public static readonly Color Lighten1 = new(0xFF42A5F5); public static readonly Color Medium = new(0xFF2196F3); public static readonly Color Darken1 = new(0xFF1E88E5); public static readonly Color Darken2 = new(0xFF1976D2); public static readonly Color Darken3 = new(0xFF1565C0); public static readonly Color Darken4 = new(0xFF0D47A1); public static readonly Color Accent1 = new(0xFF82B1FF); public static readonly Color Accent2 = new(0xFF448AFF); public static readonly Color Accent3 = new(0xFF2979FF); public static readonly Color Accent4 = new(0xFF2962FF); } public static class LightBlue { public static readonly Color Lighten5 = new(0xFFE1F5FE); public static readonly Color Lighten4 = new(0xFFB3E5FC); public static readonly Color Lighten3 = new(0xFF81D4FA); public static readonly Color Lighten2 = new(0xFF4FC3F7); public static readonly Color Lighten1 = new(0xFF29B6F6); public static readonly Color Medium = new(0xFF03A9F4); public static readonly Color Darken1 = new(0xFF039BE5); public static readonly Color Darken2 = new(0xFF0288D1); public static readonly Color Darken3 = new(0xFF0277BD); public static readonly Color Darken4 = new(0xFF01579B); public static readonly Color Accent1 = new(0xFF80D8FF); public static readonly Color Accent2 = new(0xFF40C4FF); public static readonly Color Accent3 = new(0xFF00B0FF); public static readonly Color Accent4 = new(0xFF0091EA); } public static class Cyan { public static readonly Color Lighten5 = new(0xFFE0F7FA); public static readonly Color Lighten4 = new(0xFFB2EBF2); public static readonly Color Lighten3 = new(0xFF80DEEA); public static readonly Color Lighten2 = new(0xFF4DD0E1); public static readonly Color Lighten1 = new(0xFF26C6DA); public static readonly Color Medium = new(0xFF00BCD4); public static readonly Color Darken1 = new(0xFF00ACC1); public static readonly Color Darken2 = new(0xFF0097A7); public static readonly Color Darken3 = new(0xFF00838F); public static readonly Color Darken4 = new(0xFF006064); public static readonly Color Accent1 = new(0xFF84FFFF); public static readonly Color Accent2 = new(0xFF18FFFF); public static readonly Color Accent3 = new(0xFF00E5FF); public static readonly Color Accent4 = new(0xFF00B8D4); } public static class Teal { public static readonly Color Lighten5 = new(0xFFE0F2F1); public static readonly Color Lighten4 = new(0xFFB2DFDB); public static readonly Color Lighten3 = new(0xFF80CBC4); public static readonly Color Lighten2 = new(0xFF4DB6AC); public static readonly Color Lighten1 = new(0xFF26A69A); public static readonly Color Medium = new(0xFF009688); public static readonly Color Darken1 = new(0xFF00897B); public static readonly Color Darken2 = new(0xFF00796B); public static readonly Color Darken3 = new(0xFF00695C); public static readonly Color Darken4 = new(0xFF004D40); public static readonly Color Accent1 = new(0xFFA7FFEB); public static readonly Color Accent2 = new(0xFF64FFDA); public static readonly Color Accent3 = new(0xFF1DE9B6); public static readonly Color Accent4 = new(0xFF00BFA5); } public static class Green { public static readonly Color Lighten5 = new(0xFFE8F5E9); public static readonly Color Lighten4 = new(0xFFC8E6C9); public static readonly Color Lighten3 = new(0xFFA5D6A7); public static readonly Color Lighten2 = new(0xFF81C784); public static readonly Color Lighten1 = new(0xFF66BB6A); public static readonly Color Medium = new(0xFF4CAF50); public static readonly Color Darken1 = new(0xFF43A047); public static readonly Color Darken2 = new(0xFF388E3C); public static readonly Color Darken3 = new(0xFF2E7D32); public static readonly Color Darken4 = new(0xFF1B5E20); public static readonly Color Accent1 = new(0xFFB9F6CA); public static readonly Color Accent2 = new(0xFF69F0AE); public static readonly Color Accent3 = new(0xFF00E676); public static readonly Color Accent4 = new(0xFF00C853); } public static class LightGreen { public static readonly Color Lighten5 = new(0xFFF1F8E9); public static readonly Color Lighten4 = new(0xFFDCEDC8); public static readonly Color Lighten3 = new(0xFFC5E1A5); public static readonly Color Lighten2 = new(0xFFAED581); public static readonly Color Lighten1 = new(0xFF9CCC65); public static readonly Color Medium = new(0xFF8BC34A); public static readonly Color Darken1 = new(0xFF7CB342); public static readonly Color Darken2 = new(0xFF689F38); public static readonly Color Darken3 = new(0xFF558B2F); public static readonly Color Darken4 = new(0xFF33691E); public static readonly Color Accent1 = new(0xFFCCFF90); public static readonly Color Accent2 = new(0xFFB2FF59); public static readonly Color Accent3 = new(0xFF76FF03); public static readonly Color Accent4 = new(0xFF64DD17); } public static class Lime { public static readonly Color Lighten5 = new(0xFFF9FBE7); public static readonly Color Lighten4 = new(0xFFF0F4C3); public static readonly Color Lighten3 = new(0xFFE6EE9C); public static readonly Color Lighten2 = new(0xFFDCE775); public static readonly Color Lighten1 = new(0xFFD4E157); public static readonly Color Medium = new(0xFFCDDC39); public static readonly Color Darken1 = new(0xFFC0CA33); public static readonly Color Darken2 = new(0xFFAFB42B); public static readonly Color Darken3 = new(0xFF9E9D24); public static readonly Color Darken4 = new(0xFF827717); public static readonly Color Accent1 = new(0xFFF4FF81); public static readonly Color Accent2 = new(0xFFEEFF41); public static readonly Color Accent3 = new(0xFFC6FF00); public static readonly Color Accent4 = new(0xFFAEEA00); } public static class Yellow { public static readonly Color Lighten5 = new(0xFFFFFDE7); public static readonly Color Lighten4 = new(0xFFFFF9C4); public static readonly Color Lighten3 = new(0xFFFFF59D); public static readonly Color Lighten2 = new(0xFFFFF176); public static readonly Color Lighten1 = new(0xFFFFEE58); public static readonly Color Medium = new(0xFFFFEB3B); public static readonly Color Darken1 = new(0xFFFDD835); public static readonly Color Darken2 = new(0xFFFBC02D); public static readonly Color Darken3 = new(0xFFF9A825); public static readonly Color Darken4 = new(0xFFF57F17); public static readonly Color Accent1 = new(0xFFFFFF8D); public static readonly Color Accent2 = new(0xFFFFFF00); public static readonly Color Accent3 = new(0xFFFFEA00); public static readonly Color Accent4 = new(0xFFFFD600); } public static class Amber { public static readonly Color Lighten5 = new(0xFFFFF8E1); public static readonly Color Lighten4 = new(0xFFFFECB3); public static readonly Color Lighten3 = new(0xFFFFE082); public static readonly Color Lighten2 = new(0xFFFFD54F); public static readonly Color Lighten1 = new(0xFFFFCA28); public static readonly Color Medium = new(0xFFFFC107); public static readonly Color Darken1 = new(0xFFFFB300); public static readonly Color Darken2 = new(0xFFFFA000); public static readonly Color Darken3 = new(0xFFFF8F00); public static readonly Color Darken4 = new(0xFFFF6F00); public static readonly Color Accent1 = new(0xFFFFE57F); public static readonly Color Accent2 = new(0xFFFFD740); public static readonly Color Accent3 = new(0xFFFFC400); public static readonly Color Accent4 = new(0xFFFFAB00); } public static class Orange { public static readonly Color Lighten5 = new(0xFFFFF3E0); public static readonly Color Lighten4 = new(0xFFFFE0B2); public static readonly Color Lighten3 = new(0xFFFFCC80); public static readonly Color Lighten2 = new(0xFFFFB74D); public static readonly Color Lighten1 = new(0xFFFFA726); public static readonly Color Medium = new(0xFFFF9800); public static readonly Color Darken1 = new(0xFFFB8C00); public static readonly Color Darken2 = new(0xFFF57C00); public static readonly Color Darken3 = new(0xFFEF6C00); public static readonly Color Darken4 = new(0xFFE65100); public static readonly Color Accent1 = new(0xFFFFD180); public static readonly Color Accent2 = new(0xFFFFAB40); public static readonly Color Accent3 = new(0xFFFF9100); public static readonly Color Accent4 = new(0xFFFF6D00); } public static class DeepOrange { public static readonly Color Lighten5 = new(0xFFFBE9E7); public static readonly Color Lighten4 = new(0xFFFFCCBC); public static readonly Color Lighten3 = new(0xFFFFAB91); public static readonly Color Lighten2 = new(0xFFFF8A65); public static readonly Color Lighten1 = new(0xFFFF7043); public static readonly Color Medium = new(0xFFFF5722); public static readonly Color Darken1 = new(0xFFF4511E); public static readonly Color Darken2 = new(0xFFE64A19); public static readonly Color Darken3 = new(0xFFD84315); public static readonly Color Darken4 = new(0xFFBF360C); public static readonly Color Accent1 = new(0xFFFF9E80); public static readonly Color Accent2 = new(0xFFFF6E40); public static readonly Color Accent3 = new(0xFFFF3D00); public static readonly Color Accent4 = new(0xFFDD2C00); } public static class Brown { public static readonly Color Lighten5 = new(0xFFEFEBE9); public static readonly Color Lighten4 = new(0xFFD7CCC8); public static readonly Color Lighten3 = new(0xFFBCAAA4); public static readonly Color Lighten2 = new(0xFFA1887F); public static readonly Color Lighten1 = new(0xFF8D6E63); public static readonly Color Medium = new(0xFF795548); public static readonly Color Darken1 = new(0xFF6D4C41); public static readonly Color Darken2 = new(0xFF5D4037); public static readonly Color Darken3 = new(0xFF4E342E); public static readonly Color Darken4 = new(0xFF3E2723); } public static class Grey { public static readonly Color Lighten5 = new(0xFFFAFAFA); public static readonly Color Lighten4 = new(0xFFF5F5F5); public static readonly Color Lighten3 = new(0xFFEEEEEE); public static readonly Color Lighten2 = new(0xFFE0E0E0); public static readonly Color Lighten1 = new(0xFFBDBDBD); public static readonly Color Medium = new(0xFF9E9E9E); public static readonly Color Darken1 = new(0xFF757575); public static readonly Color Darken2 = new(0xFF616161); public static readonly Color Darken3 = new(0xFF424242); public static readonly Color Darken4 = new(0xFF212121); } public static class BlueGrey { public static readonly Color Lighten5 = new(0xFFECEFF1); public static readonly Color Lighten4 = new(0xFFCFD8DC); public static readonly Color Lighten3 = new(0xFFB0BEC5); public static readonly Color Lighten2 = new(0xFF90A4AE); public static readonly Color Lighten1 = new(0xFF78909C); public static readonly Color Medium = new(0xFF607D8B); public static readonly Color Darken1 = new(0xFF546E7A); public static readonly Color Darken2 = new(0xFF455A64); public static readonly Color Darken3 = new(0xFF37474F); public static readonly Color Darken4 = new(0xFF263238); } } } ================================================ FILE: Source/QuestPDF/Helpers/FontFeatures.cs ================================================ using System; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace QuestPDF.Helpers; public static class FontFeatures { public const string AccessAllAlternates = "aalt"; public const string AboveBaseForms = "abvf"; public const string AboveBaseMarkPositioning = "abvm"; public const string AboveBaseSubstitutions = "abvs"; public const string AlternativeFractions = "afrc"; public const string Akhand = "akhn"; public const string KerningForAlternateProportionalWidths = "apkn"; public const string BelowBaseForms = "blwf"; public const string BelowBaseMarkPositioning = "blwm"; public const string BelowBaseSubstitutions = "blws"; public const string ContextualAlternates = "calt"; public const string CaseSensitiveForms = "case"; public const string GlyphCompositionDecomposition = "ccmp"; public const string ConjunctFormAfterRo = "cfar"; public const string ContextualHalfWidthSpacing = "chws"; public const string ConjunctForms = "cjct"; public const string ContextualLigatures = "clig"; public const string CenteredCjkPunctuation = "cpct"; public const string CapitalSpacing = "cpsp"; public const string ContextualSwash = "cswh"; public const string CursivePositioning = "curs"; public const string PetiteCapitalsFromCapitals = "c2pc"; public const string SmallCapitalsFromCapitals = "c2sc"; public const string Distances = "dist"; public const string DiscretionaryLigatures = "dlig"; public const string Denominators = "dnom"; public const string DotlessForms = "dtls"; public const string ExpertForms = "expt"; public const string FinalGlyphOnLineAlternates = "falt"; public const string TerminalForms2 = "fin2"; public const string TerminalForms3 = "fin3"; public const string TerminalForms = "fina"; public const string FlattenedAccentForms = "flac"; public const string Fractions = "frac"; public const string FullWidths = "fwid"; public const string HalfForms = "half"; public const string HalantForms = "haln"; public const string AlternateHalfWidths = "halt"; public const string HistoricalForms = "hist"; public const string HorizontalKanaAlternates = "hkna"; public const string HistoricalLigatures = "hlig"; public const string Hangul = "hngl"; public const string HalfWidths = "hwid"; public const string InitialForms = "init"; public const string IsolatedForms = "isol"; public const string Italics = "ital"; public const string JustificationAlternates = "jalt"; public const string JIS78Forms = "jp78"; public const string JIS83Forms = "jp83"; public const string JIS90Forms = "jp90"; public const string JIS2004Forms = "jp04"; public const string Kerning = "kern"; public const string LeftBounds = "lfbd"; public const string StandardLigatures = "liga"; public const string LeadingJamoForms = "ljmo"; public const string LiningFigures = "lnum"; public const string LocalizedForms = "locl"; public const string LeftToRightAlternates = "ltra"; public const string LeftToRightMirroredForms = "ltrm"; public const string MarkPositioning = "mark"; public const string MedialForms2 = "med2"; public const string MedialForms = "medi"; public const string MathematicalGreek = "mgrk"; public const string MarkToMarkPositioning = "mkmk"; public const string MarkPositioningViaSubstitution = "mset"; public const string AlternateAnnotationForms = "nalt"; public const string NlcKanjiForms = "nlck"; public const string NuktaForms = "nukt"; public const string Numerators = "numr"; public const string OldstyleFigures = "onum"; public const string OpticalBounds = "opbd"; public const string Ordinals = "ordn"; public const string Ornaments = "ornm"; public const string ProportionalAlternateWidths = "palt"; public const string PetiteCapitals = "pcap"; public const string ProportionalKana = "pkna"; public const string ProportionalFigures = "pnum"; public const string PreBaseForms = "pref"; public const string PreBaseSubstitutions = "pres"; public const string PostBaseForms = "pstf"; public const string PostBaseSubstitutions = "psts"; public const string ProportionalWidths = "pwid"; public const string QuarterWidths = "qwid"; public const string Randomize = "rand"; public const string RequiredContextualAlternates = "rclt"; public const string RakarForms = "rkrf"; public const string RequiredLigatures = "rlig"; public const string RephForm = "rphf"; public const string RightBounds = "rtbd"; public const string RightToLeftAlternates = "rtla"; public const string RightToLeftMirroredForms = "rtlm"; public const string RubyNotationForms = "ruby"; public const string RequiredVariationAlternates = "rvrn"; public const string StylisticAlternates = "salt"; public const string ScientificInferiors = "sinf"; public const string OpticalSize = "size"; public const string SmallCapitals = "smcp"; public const string SimplifiedForms = "smpl"; public const string MathScriptStyleAlternates = "ssty"; public const string StretchingGlyphDecomposition = "stch"; public const string Subscript = "subs"; public const string Superscript = "sups"; public const string Swash = "swsh"; public const string Titling = "titl"; public const string TrailingJamoForms = "tjmo"; public const string TraditionalNameForms = "tnam"; public const string TabularFigures = "tnum"; public const string TraditionalForms = "trad"; public const string ThirdWidths = "twid"; public const string Unicase = "unic"; public const string AlternateVerticalMetrics = "valt"; public const string KerningForAlternateProportionalVerticalMetrics = "vapk"; public const string VattuVariants = "vatu"; public const string VerticalContextualHalfWidthSpacing = "vchw"; public const string VerticalAlternates = "vert"; public const string AlternateVerticalHalfMetrics = "vhal"; public const string VowelJamoForms = "vjmo"; public const string VerticalKanaAlternates = "vkna"; public const string VerticalKerning = "vkrn"; public const string ProportionalAlternateVerticalMetrics = "vpal"; public const string VerticalAlternatesAndRotation = "vrt2"; public const string VerticalAlternatesForRotation = "vrtr"; public const string SlashedZero = "zero"; /// /// (JIS X 0212-1990 Kanji Forms) /// public const string HojoKanjiForms = "hojo"; public static string CharacterVariant(int value) { if (value < 1 || value > 99) throw new ArgumentOutOfRangeException(nameof(value), "Character Variant value must be between 1 and 99."); return $"cv{value:00}"; } public static string StylisticSet(int value) { if (value < 1 || value > 20) throw new ArgumentOutOfRangeException(nameof(value), "Character Variant value must be between 1 and 20."); return $"ss{value:00}"; } } ================================================ FILE: Source/QuestPDF/Helpers/Fonts.cs ================================================ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace QuestPDF.Helpers { /// /// Contains a collection of fonts defined by the PDF standard. /// public static class Fonts { public const string Arial = "Arial"; public const string Calibri = "Calibri"; public const string Cambria = "Cambria"; public const string Candara = "Candara"; public const string ComicSans = "Comic Sans MS"; public const string Consolas = "Consolas"; public const string Corbel = "Corbel"; public const string Courier = "Courier"; public const string CourierNew = "Courier New"; public const string Georgia = "Georgia"; public const string Impact = "Impact"; public const string Lato = "Lato"; public const string LucidaConsole = "Lucida Console"; public const string SegoeSD = "Segoe SD"; public const string SegoeUI = "Segoe UI"; public const string Tahoma = "Tahoma"; public const string TimesNewRoman = "Times New Roman"; public const string TimesRoman = "Times Roman"; public const string Trebuchet = "Trebuchet MS"; public const string Verdana = "Verdana"; } } ================================================ FILE: Source/QuestPDF/Helpers/Helpers.cs ================================================ using System; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq.Expressions; using System.Reflection; using System.Text.RegularExpressions; using QuestPDF.Drawing; using QuestPDF.Drawing.DrawingCanvases; using QuestPDF.Infrastructure; using QuestPDF.Skia; using static QuestPDF.Skia.SkSvgImageSize.Unit; namespace QuestPDF.Helpers { internal static class Helpers { static Helpers() { SkNativeDependencyCompatibilityChecker.Test(); } internal static string PrettifyName(this string text) { return Regex.Replace(text, @"([a-z])([A-Z])", "$1 $2", RegexOptions.Compiled); } internal static void VisitChildren(this Element? root, Action handler) { Traverse(root); void Traverse(Element? element) { if (element == null) return; if (element is ContainerElement containerElement) { Traverse(containerElement.Child); } else { foreach (var child in element.GetChildren()) Traverse(child); } handler(element); } } internal static void ReleaseDisposableChildren(this Element? element) { element.VisitChildren(x => (x as IDisposable)?.Dispose()); } internal static bool IsGreaterThan(this float first, float second) { return first > second + Size.Epsilon; } internal static bool IsLessThan(this float first, float second) { return first < second - Size.Epsilon; } public static bool AreClose(double a, double b) { return Math.Abs(a - b) <= Size.Epsilon; } internal static bool IsNegative(this Size size) { return size.Width < -Size.Epsilon || size.Height < -Size.Epsilon; } internal static bool IsCloseToZero(this Size size) { return Math.Abs(size.Width) < Size.Epsilon && Math.Abs(size.Height) < Size.Epsilon; } internal static bool IsEmpty(this Element element) { return element.Measure(Size.Zero).Type == SpacePlanType.Empty; } internal static int ToQualityValue(this ImageCompressionQuality quality) { return quality switch { ImageCompressionQuality.Best => 100, ImageCompressionQuality.VeryHigh => 90, ImageCompressionQuality.High => 75, ImageCompressionQuality.Medium => 50, ImageCompressionQuality.Low => 25, ImageCompressionQuality.VeryLow => 10, _ => throw new ArgumentOutOfRangeException(nameof(quality), quality, null) }; } internal static bool ToDownsamplingStrategy(this ImageCompressionQuality quality) { return quality switch { ImageCompressionQuality.Best => false, ImageCompressionQuality.VeryHigh => false, ImageCompressionQuality.High => true, ImageCompressionQuality.Medium => true, ImageCompressionQuality.Low => true, ImageCompressionQuality.VeryLow => true, _ => throw new ArgumentOutOfRangeException(nameof(quality), quality, null) }; } internal static SkImage CompressImage(this SkImage image, ImageCompressionQuality compressionQuality) { return image.ResizeAndCompress(image.Width, image.Height, compressionQuality.ToQualityValue(), compressionQuality.ToDownsamplingStrategy()); } internal static SkImage ResizeAndCompressImage(this SkImage image, ImageSize targetResolution, ImageCompressionQuality compressionQuality) { if (targetResolution.Width == 0 || targetResolution.Height == 0) targetResolution = new ImageSize(1, 1); return image.ResizeAndCompress(targetResolution.Width, targetResolution.Height, compressionQuality.ToQualityValue(), compressionQuality.ToDownsamplingStrategy()); } internal static SkImage GetImageWithSmallerSize(SkImage one, SkImage second) { return one.EncodedDataSize < second.EncodedDataSize ? one : second; } internal static void OpenFileUsingDefaultProgram(string filePath) { var process = new Process { StartInfo = new ProcessStartInfo(filePath) { UseShellExecute = true } }; process.Start(); process.WaitForExit(); } internal static string ApplicationFilesPath { get { var baseDirectory = AppContext.BaseDirectory; if (string.IsNullOrWhiteSpace(baseDirectory) || baseDirectory == "/") return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); return baseDirectory; } } internal static (float widthScale, float heightScale) CalculateSpaceScale(this SkSvgImage image, Size availableSpace) { var widthScale = CalculateDimensionScale(availableSpace.Width, image.Size.Width, image.Size.WidthUnit); var heightScale = CalculateDimensionScale(availableSpace.Height, image.Size.Height, image.Size.HeightUnit); return (widthScale, heightScale); float CalculateDimensionScale(float availableSize, float imageSize, SkSvgImageSize.Unit unit) { if (unit == Percentage) return 1; if (unit is Centimeters or Millimeters or Inches or Points or Picas) return availableSize / ConvertToPoints(imageSize, unit); return availableSize / imageSize; } float ConvertToPoints(float value, SkSvgImageSize.Unit unit) { const float inchToCentimetre = 2.54f; const float inchToPoints = 72; var points = unit switch { Centimeters => value / inchToCentimetre * inchToPoints, Millimeters => value / 10 / inchToCentimetre * inchToPoints, Inches => value * inchToPoints, Points => value, Picas => value * 12, _ => throw new ArgumentOutOfRangeException() }; // different naming schema: SVG pixel = PDF point return points * GetScalingFactor(); } float GetScalingFactor() { // in CSS dpi is set to 96, but Skia uses more traditional 90 // when the SVG ViewBox attribute is present, Skia uses legacy/traditional 90 DPI // otherwise, we should assume modern CSS-based 96 DPI for better compatibility var targetDpi = HasViewBox() ? 90f : 96f; return targetDpi / 72; } bool HasViewBox() { return image.ViewBox is not { Left : 0f, Top : 0f, Width : 0f, Height : 0f }; } } public static string FormatAsCompanionNumber(this float value) { return value.ToString("0.#", CultureInfo.InvariantCulture); } public static bool Is(this IDrawingCanvas canvas) where T : IDrawingCanvas { var canvasUnderTest = canvas; while (canvasUnderTest is ProxyDrawingCanvas proxy) canvasUnderTest = proxy.Target; return canvasUnderTest is T; } } } ================================================ FILE: Source/QuestPDF/Helpers/IsExternalInit.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; namespace System.Runtime.CompilerServices { /// /// Reserved to be used by the compiler for tracking metadata. /// This class should not be used by developers in source code. /// [EditorBrowsable(EditorBrowsableState.Never)] internal static class IsExternalInit { } } ================================================ FILE: Source/QuestPDF/Helpers/LicenseChecker.cs ================================================ using System; using System.Diagnostics; using QuestPDF.Infrastructure; namespace QuestPDF.Helpers; static class LicenseChecker { private static bool IsLicenseValidated { get; set; } = false; public static void ValidateLicense() { if (IsLicenseValidated) return; if (Settings.License == LicenseType.Evaluation) PrintLicenseEvaluationWarning(); if (Settings.License is null) ThrowExceptionWithWelcomeMessage(); IsLicenseValidated = true; } private static void PrintLicenseEvaluationWarning() { var warningMessage = "[QuestPDF] The library is running in Evaluation Mode. " + "This mode is fully functional and intended only for product evaluation and internal testing. " + "Commercial and production use requires an appropriate license. " + "For licensing details and pricing, please visit: https://www.questpdf.com/license"; try { if (TraceHasListeners()) Trace.TraceWarning(warningMessage); else Console.WriteLine($"\n{warningMessage}\n"); } catch { } } private static bool TraceHasListeners() { if (Trace.Listeners.Count == 0) return false; if (Trace.Listeners.Count == 1 && Trace.Listeners[0] is DefaultTraceListener) return false; return true; } private static void ThrowExceptionWithWelcomeMessage() { const string newParagraph = "\n\n"; var exceptionMessage = $"{newParagraph}{newParagraph}Thank you for choosing QuestPDF 👋{newParagraph}" + $"Before you continue, please take a moment to configure your license. This step helps ensure correct license compliance.{newParagraph}" + $"QuestPDF requires a Commercial License for production use by organizations with more than $1M USD in annual gross revenue. " + $"Individuals, non-profits, open-source projects, and smaller companies qualify for the free Community license.{newParagraph}" + $"If you are not the decision-maker for software purchases, please share the licensing and pricing details with your team lead or manager: https://www.questpdf.com/license {newParagraph}" + $"Available license options:\n" + $"- Community: free,\n" + $"- Evaluation: intended solely for evaluation before choosing an appropriate license; not suitable for production use,\n" + $"- Professional: paid, for teams up to 10 developers with dedicated support,\n" + $"- Enterprise: paid, for unlimited developers with prioritized dedicated support.{newParagraph}" + $"Set the license once at application startup. By doing so, you confirm that the selected tier matches your usage:\n" + $"> QuestPDF.Settings.License = LicenseType.Evaluation; // or Community / Professional / Enterprise{newParagraph}" + $"No license key or activation is required — we trust you to select the correct option. " + $"By choosing the right license, you help ensure QuestPDF remains sustainable and continuously improving for everyone. {newParagraph}" + $"We wish you a great experience! 🚀{newParagraph}{newParagraph}"; throw new Exception(exceptionMessage) { HelpLink = "https://www.questpdf.com/license" }; } } ================================================ FILE: Source/QuestPDF/Helpers/NativeDependencyCompatibilityChecker.cs ================================================ using System; using System.Linq; using System.Runtime.InteropServices; namespace QuestPDF.Helpers { internal sealed class NativeDependencyCompatibilityChecker { private static readonly Version RequiredGlibcVersionOnLinux = Version.Parse("2.29"); private bool IsCompatibilityChecked { get; set; } = false; public Action ExecuteNativeCode { get; set; } = () => { }; public Func CheckNativeLibraryVersion { get; set; } = () => true; public Func ExceptionHint { get; set; } = () => string.Empty; public void Test() { if (IsCompatibilityChecked) return; TestOnce(); IsCompatibilityChecked = true; } private void TestOnce() { if (IsCompatibilityChecked) return; const string exceptionBaseMessage = "The QuestPDF library has encountered an issue while loading one of its dependencies."; const string paragraph = "\n\n"; // test with dotnet-based mechanism where native files are provided // in the "runtimes/{rid}/native" folder on Core, or by the targets file on .NET Framework var innerException = CheckIfExceptionIsThrownWhenLoadingNativeDependencies(); if (innerException == null) { EnsureNativeVersionCompatibility(); return; } if (!NativeDependencyProvider.IsCurrentPlatformSupported()) ThrowCompatibilityException(innerException); // detect platform, copy appropriate native files and test compatibility again NativeDependencyProvider.EnsureNativeFileAvailability(); innerException = CheckIfExceptionIsThrownWhenLoadingNativeDependencies(); if (innerException == null) { EnsureNativeVersionCompatibility(); return; } ThrowCompatibilityException(innerException); void ThrowCompatibilityException(Exception innerException) { var supportedRuntimes = string.Join(", ", NativeDependencyProvider.SupportedPlatforms); var currentRuntime = NativeDependencyProvider.GetRuntimePlatform(); var isRuntimeSupported = NativeDependencyProvider.SupportedPlatforms.Contains(currentRuntime); var message = $"{exceptionBaseMessage}"; if (!isRuntimeSupported) { message += $"{paragraph}Your runtime is not supported by QuestPDF. " + $"The following runtimes are supported: {supportedRuntimes}. " + $"Your current runtime is detected as '{currentRuntime}'. "; } if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) message += $"{paragraph}Please always set the 'Platform target' to either 'X86' or 'X64' in your startup project settings. Please do not use the 'Any CPU' option."; if (RuntimeInformation.ProcessArchitecture is Architecture.Arm) message += $"{paragraph}Please consider setting the 'Platform target' property to 'Arm64' in your project settings."; var hint = ExceptionHint.Invoke(); if (!string.IsNullOrEmpty(hint)) message += $"{paragraph}{hint}"; if (isRuntimeSupported) { var glibcVersion = NativeDependencyProvider.GetGlibcVersion(); if (glibcVersion != null && glibcVersion < RequiredGlibcVersionOnLinux) { message += $"{paragraph}Please consider updating your operating system distribution. " + $"Current GLIBC version: {glibcVersion}. " + $"The minimum required version is {RequiredGlibcVersionOnLinux}. "; } else { message += $"{paragraph}If the problem persists, it may mean that your current operating system distribution is outdated. For optimal compatibility, please consider updating it to a more recent version."; } } throw new Exception(message, innerException); } void EnsureNativeVersionCompatibility() { if (!CheckNativeLibraryVersion()) throw new Exception($"{exceptionBaseMessage}{paragraph}The loaded native library version is incompatible with the current QuestPDF version. To resolve this issue, please: 1) Clean and rebuild your solution, 2) Remove the bin and obj folders, and 3) Ensure all projects in your solution use the same QuestPDF NuGet package version."); } } private Exception? CheckIfExceptionIsThrownWhenLoadingNativeDependencies() { try { ExecuteNativeCode(); return null; } catch (Exception exception) { return exception; } } } } ================================================ FILE: Source/QuestPDF/Helpers/NativeDependencyProvider.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace QuestPDF.Helpers; internal static class NativeDependencyProvider { public static readonly string[] SupportedPlatforms = { "win-x86", "win-x64", "linux-x64", "linux-arm64", "linux-musl-x64", "osx-x64", "osx-arm64" }; public static void EnsureNativeFileAvailability() { var nativeFilesPath = GetNativeFileSourcePath(); if (nativeFilesPath == null) return; foreach (var nativeFilePath in Directory.GetFiles(nativeFilesPath)) { var targetDirectory = new FileInfo(nativeFilePath) .Directory .Parent // native .Parent // platform .Parent // runtimes .FullName; var targetPath = Path.Combine(targetDirectory, Path.GetFileName(nativeFilePath)); CopyFileIfNewer(nativeFilePath, targetPath); } } public static bool IsCurrentPlatformSupported() { var currentRuntime = GetRuntimePlatform(); return SupportedPlatforms.Contains(currentRuntime); } static string? GetNativeFileSourcePath() { var platform = GetRuntimePlatform(); var availableLocations = new[] { AppDomain.CurrentDomain.RelativeSearchPath, AppDomain.CurrentDomain.BaseDirectory, Environment.CurrentDirectory, AppContext.BaseDirectory, Directory.GetCurrentDirectory(), new FileInfo(typeof(NativeDependencyProvider).Assembly.Location).Directory?.FullName }; foreach (var location in availableLocations) { if (string.IsNullOrEmpty(location)) continue; var nativeFileSourcePath = Path.Combine(location, "runtimes", platform, "native"); if (Directory.Exists(nativeFileSourcePath)) return nativeFileSourcePath; } return null; } public static string GetRuntimePlatform() { #if NET6_0_OR_GREATER if (RuntimeInformation.ProcessArchitecture == Architecture.Wasm) return "browser-wasm"; #endif return $"{GetSystemIdentifier()}-{GetProcessArchitecture()}"; static string GetSystemIdentifier() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "win"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return IsLinuxMusl() ? "linux-musl" : "linux"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "osx"; return "other"; } static string GetProcessArchitecture() { return RuntimeInformation.ProcessArchitecture.ToString().ToLower(); } } private static string? ExecuteLddCommand() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return null; try { var processStartInfo = new ProcessStartInfo { FileName = "ldd", Arguments = "--version", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var process = Process.Start(processStartInfo); if (process == null) return null; var standardOutputText = process.StandardOutput.ReadToEnd(); var standardErrorText = process.StandardError.ReadToEnd(); process.WaitForExit(); return standardOutputText + standardErrorText; } catch { return null; } } private static bool IsLinuxMusl() { var lddCommandOutput = ExecuteLddCommand() ?? string.Empty; var containsMuslText = lddCommandOutput.IndexOf("musl", StringComparison.InvariantCultureIgnoreCase) >= 0; return containsMuslText; } public static Version? GetGlibcVersion() { var lddCommandOutput = ExecuteLddCommand() ?? string.Empty; var match = Regex.Match(lddCommandOutput, @"ldd \(.+\) (?[1-9]\.[0-9]{2})"); if (!match.Success) return null; var versionGroup = match.Groups["version"]; if (!versionGroup.Success) return null; return Version.TryParse(versionGroup.Value, out var parsedVersion) ? parsedVersion : null; } private static void CopyFileIfNewer(string sourcePath, string targetPath) { if (!File.Exists(sourcePath)) throw new FileNotFoundException($"Source file not found: {sourcePath}"); if (!File.Exists(targetPath) || File.GetLastWriteTime(sourcePath) > File.GetLastWriteTime(targetPath)) File.Copy(sourcePath, targetPath, true); } } ================================================ FILE: Source/QuestPDF/Helpers/PageSizes.cs ================================================ using System; using QuestPDF.Infrastructure; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace QuestPDF.Helpers { /// /// Defines the physical dimensions (width and height) of a page. /// /// /// Commonly used page sizes are available in the class. /// Change page orientation with the Portrait and Landscape extension methods. /// /// /// PageSizes.A4.Landscape(); /// public sealed class PageSize { public readonly float Width; public readonly float Height; public PageSize(float width, float height, Unit unit = Unit.Point) { if (width < 0) throw new ArgumentOutOfRangeException(nameof(width), "Page width must be greater than 0."); if (height < 0) throw new ArgumentOutOfRangeException(nameof(height), "Page height must be greater than 0."); Width = width.ToPoints(unit); Height = height.ToPoints(unit); } public static implicit operator Size(PageSize pageSize) => new Size(pageSize.Width, pageSize.Height); } /// /// Contains a collection of predefined, common and standard page sizes, such as A4 with dimensions of 595 points in width and 842 points in height. /// public static class PageSizes { public const int PointsPerInch = 72; public static PageSize A0 { get; } = new(2384, 3370); public static PageSize A1 { get; } = new(1684, 2384); public static PageSize A2 { get; } = new(1191, 1684); public static PageSize A3 { get; } = new(842, 1191); public static PageSize A4 { get; } = new(595, 842); public static PageSize A5 { get; } = new(420, 595); public static PageSize A6 { get; } = new(298, 420); public static PageSize A7 { get; } = new(210, 298); public static PageSize A8 { get; } = new(147, 210); public static PageSize A9 { get; } = new(105, 147); public static PageSize A10 { get; } = new(74, 105); public static PageSize B0 { get; } = new(2835, 4008); public static PageSize B1 { get; } = new(2004, 2835); public static PageSize B2 { get; } = new(1417, 2004); public static PageSize B3 { get; } = new(1001, 1417); public static PageSize B4 { get; } = new(709, 1001); public static PageSize B5 { get; } = new(499, 709); public static PageSize B6 { get; } = new(354, 499); public static PageSize B7 { get; } = new(249, 354); public static PageSize B8 { get; } = new(176, 249); public static PageSize B9 { get; } = new(125, 176); public static PageSize B10 { get; } = new(88, 125); public static PageSize C0 { get; } = new(2599, 3677); public static PageSize C1 { get; } = new(1837, 2599); public static PageSize C2 { get; } = new(1298, 1837); public static PageSize C3 { get; } = new(918, 1298); public static PageSize C4 { get; } = new(649, 918); public static PageSize C5 { get; } = new(459, 649); public static PageSize C6 { get; } = new(323, 459); public static PageSize C7 { get; } = new(230, 323); public static PageSize C8 { get; } = new(162, 230); public static PageSize C9 { get; } = new(113, 162); public static PageSize C10 { get; } = new(79, 113); public static PageSize Env10 { get; } = new(297, 684); public static PageSize EnvC4 { get; } = new(649, 918); public static PageSize EnvDL { get; } = new(312, 624); public static PageSize Postcard { get; } = new(284, 419); public static PageSize Executive { get; } = new(522, 756); public static PageSize Letter { get; } = new(612, 792); public static PageSize Legal { get; } = new(612, 1008); public static PageSize Ledger { get; } = new(792, 1224); public static PageSize Tabloid { get; } = new(1224, 792); public static PageSize ARCH_A { get; } = new(648, 864); public static PageSize ARCH_B { get; } = new(864, 1296); public static PageSize ARCH_C { get; } = new(1296, 1728); public static PageSize ARCH_D { get; } = new(1728, 2592); public static PageSize ARCH_E { get; } = new(2592, 3456); public static PageSize ARCH_E1 { get; } = new(2160, 3024); public static PageSize ARCH_E2 { get; } = new(1872, 2736); public static PageSize ARCH_E3 { get; } = new(1944, 2808); } public static class PageSizeExtensions { /// /// Sets page size to a portrait orientation, making the width smaller than the height. /// public static PageSize Portrait(this PageSize size) { return new PageSize(Math.Min(size.Width, size.Height), Math.Max(size.Width, size.Height)); } /// /// Sets page size to a landscape orientation, making the width bigger than the height. /// public static PageSize Landscape(this PageSize size) { return new PageSize(Math.Max(size.Width, size.Height), Math.Min(size.Width, size.Height)); } } } ================================================ FILE: Source/QuestPDF/Helpers/Placeholders.cs ================================================ using System; using System.IO; using System.Linq; using System.Reflection; using QuestPDF.Infrastructure; using QuestPDF.Skia; namespace QuestPDF.Helpers { public static class Placeholders { static Placeholders() { SkNativeDependencyCompatibilityChecker.Test(); } public static readonly Random Random = new Random(); #region Word Cache private const string CommonParagraph = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod " + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " + "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate " + "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " + "occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + "mollit anim id est laborum."; private static readonly string[] LatinWords = LoadLatinWords(); private static readonly string[] LongLatinWords = LatinWords.Where(x => x.Length > 8).ToArray(); private static string[] LoadLatinWords() { using var stream = Assembly .GetExecutingAssembly() .GetManifestResourceStream("QuestPDF.Resources.LatinWords.txt"); using var streamReader = new StreamReader(stream); var text = streamReader.ReadToEnd(); return text.Split('\n').ToArray(); } #endregion #region Text private static string RandomWord() { var index = Random.Next(0, LatinWords.Length); return LatinWords[index]; } private static string LongRandomWord() { var index = Random.Next(0, LongLatinWords.Length); return LongLatinWords[index]; } private static string RandomWords(int min, int max) { var length = Random.Next(min, max + 1); var words = Enumerable .Range(0, length) .Select(x => RandomWord()); return string.Join(" ", words); } /// /// Returns the commonly used 'Lorem ipsum dolor sit amet' placeholder text. /// public static string LoremIpsum() { return CommonParagraph; } /// /// Generates a random text ideal for concise labels like product names. /// /// /// "Beatae dolor"
/// "Rerum quibusdam perspiciatis"
/// "Fugiat aperiam officiis" ///
public static string Label() { return RandomWords(2, 3).FirstCharToUpper(); } /// /// Generates random text ideal for single sentences, like product description. /// /// /// Vero a id optio consequuntur dignissimos repellendus provident blanditiis. /// public static string Sentence() { return RandomWords(6, 12).FirstCharToUpper() + "."; } /// /// Generates random text formatted as a question. /// /// /// Sequi enim voluptas quasi modi aspernatur dolorem? /// public static string Question() { return RandomWords(4, 8).FirstCharToUpper() + "?"; } /// /// Generates random text suited for paragraphs, like detailed product description. /// public static string Paragraph() { var length = Random.Next(3, 6); var sentences = Enumerable .Range(0, length) .Select(x => Sentence()); return string.Join(" ", sentences); } /// /// Generates random text ideal for multiple paragraphs, resembling an article. /// public static string Paragraphs() { var length = Random.Next(2, 5); var sentences = Enumerable .Range(0, length) .Select(x => Paragraph()); return string.Join("\n", sentences); } /// /// Generates random text in the format of an email address. /// /// /// consequuntur35@blanditiis.com /// public static string Email() { return $"{LongRandomWord()}{Random.Next(10, 99)}@{LongRandomWord()}.com"; } /// /// Generates random text looking like a two-word name, with capitalized initials. /// /// /// "Voluptates Inventore"
/// "Praesentium Consectetur"
/// "Voluptatibus Molestias"
///
public static string Name() { return LongRandomWord().FirstCharToUpper() + " " + LongRandomWord().FirstCharToUpper(); } /// /// Generates random text in the format of a phone number. /// /// /// 180-204-1358 /// public static string PhoneNumber() { return $"{Random.Next(100, 999)}-{Random.Next(100, 999)}-{Random.Next(1000, 9999)}"; } /// /// Generates random text resembling a webpage address. /// /// /// www.libero.com /// public static string WebpageUrl() { return $"www.{LongRandomWord()}.com"; } /// /// Generates random text resembling a price value. /// /// /// $12.99 /// public static string Price() { var price = (decimal) Math.Round(Random.NextDouble() * 100) + 0.99m; return $"{price:C}"; } private static string FirstCharToUpper(this string text) { return text.First().ToString().ToUpper() + text.Substring(1); } #endregion #region Time private static DateTime RandomDate() { var dayOffset = (Random.NextDouble() - 0.5) * 1000; return System.DateTime.Now - TimeSpan.FromDays(dayOffset); } /// /// Generates random text representation of a random time. /// /// /// 18:34:47 /// public static string Time() { return RandomDate().ToString("T"); } /// /// Generates random text that resembles a date value using short formatting. /// /// /// 04/09/2023 /// public static string ShortDate() { return RandomDate().ToString("d"); } /// /// Generates random text that resembles a full date value. /// /// /// Monday, 18 November 2024 /// public static string LongDate() { return RandomDate().ToString("D"); } /// /// Generates random text that resembles a datetime value. /// /// /// 04/03/2024 20:43:15 /// public static string DateTime() { return RandomDate().ToString("G"); } #endregion #region Numbers /// /// Generates random text mimicking an integer value, ranging from 0 to 10,000. /// public static string Integer() { return Random.Next(0, 10_000).ToString(); } /// /// Generates random text in the style of a local-formatted decimal, values from 0 to 100 with two decimal points precision. /// /// /// 1,28
/// 7,94
/// 67,30 ///
public static string Decimal() { return (Random.NextDouble() * Random.Next(0, 100)).ToString("N2"); } /// /// Generates random text resembling a percentage value. /// /// /// 48%
/// 14%
/// 23% ///
public static string Percent() { return (Random.NextDouble() * 100).ToString("N0") + "%"; } #endregion #region Visual private static readonly Color[] BackgroundColors = { Colors.Red.Lighten3, Colors.Pink.Lighten3, Colors.Purple.Lighten3, Colors.DeepPurple.Lighten3, Colors.Indigo.Lighten3, Colors.Blue.Lighten3, Colors.LightBlue.Lighten3, Colors.Cyan.Lighten3, Colors.Teal.Lighten3, Colors.Green.Lighten3, Colors.LightGreen.Lighten3, Colors.Lime.Lighten3, Colors.Yellow.Lighten3, Colors.Amber.Lighten3, Colors.Orange.Lighten3, Colors.DeepOrange.Lighten3, Colors.Brown.Lighten3, Colors.Grey.Lighten3, Colors.BlueGrey.Lighten3 }; /// /// Returns a random bright color from the Material Design palette. /// /// /// #ffab91
/// #bcaaa4
/// #ffab91 ///
public static Color BackgroundColor() { var index = Random.Next(0, BackgroundColors.Length); return BackgroundColors[index]; } /// /// Returns a random color from the Material Design palette. /// /// /// #9e9e9e
/// #f44336
/// #9c27b0 ///
public static Color Color() { var colors = new[] { Colors.Red.Medium, Colors.Pink.Medium, Colors.Purple.Medium, Colors.DeepPurple.Medium, Colors.Indigo.Medium, Colors.Blue.Medium, Colors.LightBlue.Medium, Colors.Cyan.Medium, Colors.Teal.Medium, Colors.Green.Medium, Colors.LightGreen.Medium, Colors.Lime.Medium, Colors.Yellow.Medium, Colors.Amber.Medium, Colors.Orange.Medium, Colors.DeepOrange.Medium, Colors.Brown.Medium, Colors.Grey.Medium, Colors.BlueGrey.Medium }; var index = Random.Next(0, colors.Length); return colors[index]; } /// /// Generates a random image with a soft color gradient with provided and . /// /// /// Caution: using this method may significantly reduce document generation performance. Please do not use it when performing benchmarks. /// /// Random image encoded in the JPEG format. public static byte[] Image(int width, int height) { return Image(new ImageSize(width, height)); } /// /// Generates a random image with a soft color gradient. /// /// /// For performance reasons, this method may reduce the argument to at most 64 pixels, while preserving its aspect ratio. /// /// Random image encoded in the JPEG format. public static byte[] Image(ImageSize size) { size = LimitSize(size); var colors = BackgroundColors .OrderBy(_ => Random.Next()) .Take(2) .ToArray(); using var placeholderImage = SkImage.GeneratePlaceholder(size.Width, size.Height, colors[0], colors[1]); using var imageData = placeholderImage.GetEncodedData(); return imageData.ToBytes(); static ImageSize LimitSize(ImageSize size, int maxSize = 64) { if (size.Width < maxSize && size.Height < maxSize) return size; return size.Width > size.Height ? new ImageSize(maxSize, maxSize * size.Height / size.Width) : new ImageSize(maxSize * size.Width / size.Height, maxSize); } } #endregion } } ================================================ FILE: Source/QuestPDF/Helpers/TemporaryStorage.cs ================================================ using System; using System.IO; using System.Linq; namespace QuestPDF.Helpers; internal static class TemporaryStorage { private static string? TemporaryStoragePath { get; set; } internal static string GetPath() { var path = TryGetPath(); if (path == null) { throw new InvalidOperationException( "Unable to find a suitable temporary storage location. " + "Please specify it using the Settings.TemporaryStoragePath setting and ensure that the application has permissions to read and write to that location."); } return path; } internal static string? TryGetPath() { if (TemporaryStoragePath != null) return TemporaryStoragePath; var candidates = new[] { Settings.TemporaryStoragePath, Path.Combine(Path.GetTempPath(), "QuestPDF", "temp"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "QuestPDF", "temp"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "QuestPDF", "temp") }; TemporaryStoragePath = candidates .Where(x => x != null) .FirstOrDefault(HasPermissionsToAlterPath); return TemporaryStoragePath; } private static bool HasPermissionsToAlterPath(string path) { try { if (!Directory.Exists(path)) Directory.CreateDirectory(path); var testFile = Path.Combine(path, Path.GetRandomFileName()); using (var fileStream = File.Create(testFile)) { fileStream.WriteByte(123); // write anything } File.Delete(testFile); return true; } catch { return false; } } } ================================================ FILE: Source/QuestPDF/Infrastructure/AspectRatioOption.cs ================================================ using QuestPDF.Fluent; namespace QuestPDF.Infrastructure { public enum AspectRatioOption { /// /// Adjusts content to occupy the full width available. /// /// /// Used as the default setting in the library. /// FitWidth, /// /// Adjusts content to fill the available height. /// /// /// Often used with height-constraining elements such as: Height, MaxHeight, etc. /// FitHeight, /// /// Adjusts content to fill the available area while maintaining its aspect ratio. This may result in the content fully occupying either the width or height, depending on its dimensions. /// /// /// Often used with constraining elements such as: Width, MaxWidth, Height, MaxHeight, etc. /// FitArea } } ================================================ FILE: Source/QuestPDF/Infrastructure/BoxShadowStyle.cs ================================================ using System; using QuestPDF.Helpers; namespace QuestPDF.Infrastructure; /// /// Represents the visual styling properties for a box shadow effect. /// public sealed class BoxShadowStyle { /// /// Gets or sets the horizontal offset of the shadow in pixels. /// Positive values move the shadow to the right, negative values move it to the left. /// public float OffsetX { get; set; } /// /// Gets or sets the vertical offset of the shadow in pixels. /// Positive values move the shadow downward, negative values move it upward. /// public float OffsetY { get; set; } /// /// Gets or sets the spread radius of the shadow in pixels. /// Positive values cause the shadow to expand, negative values cause it to contract. /// public float Spread { get; set; } /// /// Gets or sets the blur radius of the shadow in pixels. /// Higher values produce a more diffused shadow with softer edges. /// A value of 0 results in a sharp, unblurred shadow. /// /// /// Values different from 0 may significantly impact performance and enlarge the output file size. /// Use with caution, especially in large documents or when rendering complex shadows. /// public float Blur { get; set; } /// /// Gets or sets the color of the shadow. /// public Color Color { get; set; } = Colors.Grey.Medium; internal void Validate() { if (Blur < 0) throw new ArgumentException("Shadow blur radius cannot be negative."); } } ================================================ FILE: Source/QuestPDF/Infrastructure/Color.cs ================================================ using System; using QuestPDF.Helpers; namespace QuestPDF.Infrastructure; public readonly struct Color { public uint Hex { get; } public byte Alpha => (byte) ((Hex & 0xFF000000) >> 24); public byte Red => (byte) ((Hex & 0xFF0000) >> 16); public byte Green => (byte) ((Hex & 0x00FF00) >> 8); public byte Blue => (byte) (Hex & 0x0000FF); /// /// Creates a new color from a hex value following the ARGB format. /// For example 0xFF0000FF represents a fully opaque blue color. /// public Color(uint hex) { Hex = hex; } /// /// Creates a new color instance with the specified alpha transparency value. /// The alpha value should be within the range 0 to 255, where 0 represents fully transparent /// and 255 represents fully opaque. /// /// A new color instance with the adjusted alpha transparency. public Color WithAlpha(byte alpha) { var newHex = (Hex & 0x00FFFFFF) | ((uint)alpha << 24); return new Color(newHex); } /// /// Creates a new color instance with the specified alpha transparency value. /// The alpha value should be within the range 0 to 1, where 0 represents fully transparent /// and 1 represents fully opaque. /// /// A new color instance with the adjusted alpha transparency. public Color WithAlpha(float alpha) { if (alpha < 0 || alpha > 1) throw new ArgumentOutOfRangeException(nameof(alpha), "Alpha value must be between 0 and 1."); var newAlpha = (byte)(255 * alpha); return WithAlpha(newAlpha); } /// /// Creates a new color instance from a hex string representation. /// The string should follow the formats: #RGB, #ARGB, #RRGGBB, or #AARRGGBB. /// The hash sign is optional; for example: RGB, ARGB, RRGGBB, or AARRGGBB are also valid. /// /// The hex string representing the color. /// A new color instance created from the hex string. public static Color FromHex(string hex) { return ColorParser.ParseColorHex(hex); } /// /// Creates a new color instance given the red, green, and blue component values. /// Each component should be within the range 0 to 255. /// public static Color FromRGB(byte red, byte green, byte blue) { return FromARGB(255, red, green, blue); } /// /// Creates a new color instance from the specified alpha, red, green, and blue component values. /// Each component should be within the range 0 to 255. /// public static Color FromARGB(byte alpha, byte red, byte green, byte blue) { return new Color((uint) (alpha << 24 | red << 16 | green << 8 | blue)); } public static implicit operator string(Color color) { return color.ToString(); } public static implicit operator Color(string hex) { return FromHex(hex); } public static implicit operator uint(Color color) { return color.Hex; } public static implicit operator Color(uint hex) { if (hex <= 0x00FFFFFF) hex |= 0xFF000000; return new Color(hex); } public override string ToString() { if (Alpha == 0xFF) return $"#{Red:X2}{Green:X2}{Blue:X2}"; return $"#{Alpha:X2}{Red:X2}{Green:X2}{Blue:X2}"; } } ================================================ FILE: Source/QuestPDF/Infrastructure/ContainerElement.cs ================================================ using System; using System.Collections.Generic; using QuestPDF.Drawing; using QuestPDF.Elements; namespace QuestPDF.Infrastructure { internal abstract class ContainerElement : Element, IContainer { internal Element Child { get; set; } = Empty.Instance; IElement IContainer.Child { get => Child; set => Child = value as Element; } internal override IEnumerable GetChildren() { yield return Child; } internal override void CreateProxy(Func create) { Child = create(Child); } internal override SpacePlan Measure(Size availableSpace) { var measurement = Child.Measure(availableSpace); if (measurement.Type == SpacePlanType.Wrap) return SpacePlan.Wrap("Forwarded from child"); return measurement; } internal override void Draw(Size availableSpace) { Child?.Draw(availableSpace); } } } ================================================ FILE: Source/QuestPDF/Infrastructure/ContentDirection.cs ================================================ namespace QuestPDF.Infrastructure { public enum ContentDirection { /// /// Sets the left-to-right (LTR) content direction. /// Learn more /// /// LeftToRight, /// /// Sets the right-to-left (RTL) content direction. /// Learn more /// /// RightToLeft } } ================================================ FILE: Source/QuestPDF/Infrastructure/DocumentMetadata.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; namespace QuestPDF.Infrastructure { public sealed class DocumentMetadata { /// /// Represents the main heading or name of the document, often displayed as a prominent identifier or label in PDF metadata. /// public string? Title { get; set; } /// /// Specifies the individual or entity responsible for creating the document. /// public string? Author { get; set; } /// /// Provides a brief description or main topic related to the document content. /// public string? Subject { get; set; } /// /// Defines a collection of terms or phrases that describe the document's content or purpose. /// Improves categorization and searchability of the document. /// public string? Keywords { get; set; } /// /// Identifies the software or system that generated the document. /// public string? Creator { get; set; } /// /// Specifies the name of the application or library that generated the document. /// public string? Producer { get; set; } /// /// Specifies the language of the document content, defined using language tags such as "en-US" for American English. /// public string? Language { get; set; } /// /// Represents the date and time when the document was created. /// This property is used to specify the creation timestamp. /// public DateTimeOffset CreationDate { get; set; } = DateTimeOffset.Now; /// /// Stores the most recent date and time when the content or metadata of the document was updated. /// It is used to provide metadata information about the last revision of the document. /// public DateTimeOffset ModifiedDate { get; set; } = DateTimeOffset.Now; public static DocumentMetadata Default => new DocumentMetadata(); #region Deprecated properties [Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.DocumentLayoutExceptionThreshold static property.")] [ExcludeFromCodeCoverage] public int DocumentLayoutExceptionThreshold { get => Settings.DocumentLayoutExceptionThreshold; set => Settings.DocumentLayoutExceptionThreshold = value; } [Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableCaching static property.")] [ExcludeFromCodeCoverage] public bool ApplyCaching { get => Settings.EnableCaching; set => Settings.EnableCaching = value; } [Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableDebugging static property.")] [ExcludeFromCodeCoverage] public bool ApplyDebugging { get => Settings.EnableDebugging; set => Settings.EnableDebugging = value; } [Obsolete("This API has been moved since version 2023.5. Please use the QuestPDF.Infrastructure.DocumentSettings API.")] [ExcludeFromCodeCoverage] public int? ImageQuality { get; set; } [Obsolete("This API has been moved since version 2023.5. Please use the QuestPDF.Infrastructure.DocumentSettings API.")] [ExcludeFromCodeCoverage] public int? RasterDpi { get; set; } [Obsolete("This API has been moved since version 2023.5. Please use the QuestPDF.Infrastructure.DocumentSettings API.")] [ExcludeFromCodeCoverage] public bool? PdfA { get; set; } #endregion } } ================================================ FILE: Source/QuestPDF/Infrastructure/DocumentSettings.cs ================================================ using System; namespace QuestPDF.Infrastructure { public sealed class DocumentSettings { public const int DefaultRasterDpi = 72; [Obsolete("Please use the ConformanceLevel property instead.")] public bool PdfA { get => PDFA_Conformance != PDFA_Conformance.None; set => PDFA_Conformance = value ? PDFA_Conformance.PDFA_3B : PDFA_Conformance.None; } /// /// Gets or sets the PDF/A conformance level for the document. /// This property determines the adherence of the generated PDF to specific archival standards. /// public PDFA_Conformance PDFA_Conformance { get; set; } = PDFA_Conformance.None; /// /// Gets or sets the conformance level for PDF/UA (Universal Accessibility) compliance. /// Warning: this setting makes the document non-reproducable. /// public PDFUA_Conformance PDFUA_Conformance { get; set; } = PDFUA_Conformance.None; /// /// Gets or sets a value indicating whether the generated document should be additionally compressed. May greatly reduce file size with a small increase in generation time. /// public bool CompressDocument { get; set; } = true; /// /// Encoding quality controls the trade-off between size and quality. /// When the image is opaque, it will be encoded using the JPEG format with the selected quality setting. /// When the image contains an alpha channel, it is always encoded using the PNG format and this option is ignored. /// The default value is "high quality". /// /// /// This setting is taken into account only when the image is in the JPG format, otherwise it is ignored. /// public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.High; /// /// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized. /// A larger DPI would create a PDF that reflects the original intent with better fidelity, but it can make for larger PDF files too, which would use more memory while rendering, and it would be slower to be processed or sent online or to printer. /// When generating images, this parameter also controls the resolution of the generated content. /// Default value is 288. /// public int ImageRasterDpi { get; set; } = DefaultRasterDpi * 4; public ContentDirection ContentDirection { get; set; } = ContentDirection.LeftToRight; public static DocumentSettings Default => new DocumentSettings(); } public enum PDFA_Conformance { None = 0, // PDFA_1A = 1, // PDFA_1B = 2, PDFA_2A = 3, PDFA_2B = 4, PDFA_2U = 5, PDFA_3A = 6, PDFA_3B = 7, PDFA_3U = 8 } public enum PDFUA_Conformance { None = 0, PDFUA_1 = 1 } } ================================================ FILE: Source/QuestPDF/Infrastructure/Element.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using QuestPDF.Drawing; namespace QuestPDF.Infrastructure { internal abstract class Element : IElement { internal IPageContext PageContext { get; set; } internal IDrawingCanvas Canvas { get; set; } internal SourceCodePath? CodeLocation { get; set; } internal virtual IEnumerable GetChildren() { yield break; } internal virtual void CreateProxy(Func create) { } internal abstract SpacePlan Measure(Size availableSpace); internal abstract void Draw(Size availableSpace); internal virtual string? GetCompanionHint() => null; internal virtual string? GetCompanionSearchableContent() => null; internal virtual IEnumerable>? GetCompanionProperties() => null; } } ================================================ FILE: Source/QuestPDF/Infrastructure/EmptyContainer.cs ================================================ using QuestPDF.Elements; namespace QuestPDF.Infrastructure; public static class EmptyContainer { /// /// Creates an empty IContainer instance. /// public static IContainer Create() => new Container(); } ================================================ FILE: Source/QuestPDF/Infrastructure/FontPosition.cs ================================================ namespace QuestPDF.Infrastructure { internal enum FontPosition { Normal, Subscript, Superscript, } } ================================================ FILE: Source/QuestPDF/Infrastructure/FontWeight.cs ================================================ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace QuestPDF.Infrastructure { public enum FontWeight { Thin = 100, ExtraLight = 200, Light = 300, Normal = 400, Medium = 500, SemiBold = 600, Bold = 700, ExtraBold = 800, Black = 900, ExtraBlack = 1000 } } ================================================ FILE: Source/QuestPDF/Infrastructure/HorizontalAlignment.cs ================================================ namespace QuestPDF.Infrastructure { public enum HorizontalAlignment { Left, Center, Right } } ================================================ FILE: Source/QuestPDF/Infrastructure/IComponent.cs ================================================ namespace QuestPDF.Infrastructure { /// /// This interface represents a reusable document fragment. /// /// Components serve as modular building blocks for abstracting document layouts. /// They promote code reusability across multiple sections or types of documents. /// Using a component, you can generate content based on its internal state. /// /// /// /// Consider the scenario of a company-wide page header. /// Instead of replicating the same header design across various documents, a single component can be created and referenced wherever needed. /// public interface IComponent { /// /// Method invoked by the library to compose document content. /// void Compose(IContainer container); } } ================================================ FILE: Source/QuestPDF/Infrastructure/IContainer.cs ================================================ using QuestPDF.Elements; namespace QuestPDF.Infrastructure { /// /// Represents a layout structure with exactly one child element. /// /// /// The main purpose of this interface is to facilitate the Fluent API's construction. /// It's not intended to allow external creation of new container kinds or layout designs. /// public interface IContainer { IElement? Child { get; set; } } } ================================================ FILE: Source/QuestPDF/Infrastructure/IContentDirectionAware.cs ================================================ namespace QuestPDF.Infrastructure { internal interface IContentDirectionAware { public ContentDirection ContentDirection { get; set; } } } ================================================ FILE: Source/QuestPDF/Infrastructure/IDocument.cs ================================================ namespace QuestPDF.Infrastructure { /// /// Represents the document abstraction, including its content, metadata, and configuration settings. /// Learn more /// /// /// Implement this interface to centralize your entire document's structure within a single class for easy management. /// For a different approach, consider the Minimal API pathway. /// public interface IDocument { #if NETCOREAPP3_0_OR_GREATER /// /// Provides metadata values like author and keywords used in PDF creation. /// /// /// Override this method to customize document's metadata. /// public DocumentMetadata GetMetadata() => DocumentMetadata.Default; /// /// Provides document generation settings, such as default image DPI and compression rate. /// /// /// Override this to customize default configurations. /// public DocumentSettings GetSettings() => DocumentSettings.Default; #else DocumentMetadata GetMetadata(); DocumentSettings GetSettings(); #endif /// /// Configures the document content by specifying its layout structure and visual element. /// /// The document container used for defining content via the FluentAPI. void Compose(IDocumentContainer container); } } ================================================ FILE: Source/QuestPDF/Infrastructure/IDocumentCanvas.cs ================================================ using QuestPDF.Drawing; namespace QuestPDF.Infrastructure { internal interface IDocumentCanvas { void SetSemanticTree(SemanticTreeNode? semanticTree); void BeginDocument(); void EndDocument(); void BeginPage(Size size); void EndPage(); IDrawingCanvas GetDrawingCanvas(); } } ================================================ FILE: Source/QuestPDF/Infrastructure/IDocumentContainer.cs ================================================ namespace QuestPDF.Infrastructure { /// /// Represents the primary container encapsulating the entire content of the document. /// public interface IDocumentContainer { } } ================================================ FILE: Source/QuestPDF/Infrastructure/IDrawingCanvas.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Skia; using QuestPDF.Skia.Text; namespace QuestPDF.Infrastructure { internal interface IDrawingCanvas { DocumentPageSnapshot GetSnapshot(); void DrawSnapshot(DocumentPageSnapshot snapshot); void Save(); void Restore(); void SetZIndex(int index); int GetZIndex(); SkCanvasMatrix GetCurrentMatrix(); void SetMatrix(SkCanvasMatrix matrix); void Translate(Position vector); void Scale(float scaleX, float scaleY); void Rotate(float angle); void DrawLine(Position start, Position end, SkPaint paint); void DrawRectangle(Position vector, Size size, SkPaint paint); void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint); void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow); void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo); void DrawImage(SkImage image, Size size); void DrawPicture(SkPicture picture); void DrawSvgPath(string path, Color color); void DrawSvg(SkSvgImage svgImage, Size size); void DrawOverflowArea(SkRect area); void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace); void ClipRectangle(SkRect clipArea); void ClipRoundedRectangle(SkRoundedRect clipArea); void DrawHyperlink(Size size, string url, string? description); void DrawSectionLink(Size size, string sectionName, string? description); void DrawSection(string sectionName); int GetSemanticNodeId(); void SetSemanticNodeId(int nodeId); } } ================================================ FILE: Source/QuestPDF/Infrastructure/IDynamicComponent.cs ================================================ using System; using QuestPDF.Elements; namespace QuestPDF.Infrastructure { internal sealed class DynamicComponentProxy { internal Action SetState { get; private set; } internal Func GetState { get; private set; } internal Func Compose { get; private set; } internal static DynamicComponentProxy CreateFrom(IDynamicComponent component) where TState : struct { return new DynamicComponentProxy { GetState = () => component.State, SetState = x => component.State = (TState)x, Compose = component.Compose }; } internal static DynamicComponentProxy CreateFrom(IDynamicComponent component) { return new DynamicComponentProxy { GetState = () => null, SetState = _ => { }, Compose = component.Compose }; } } /// /// Represents the output from the DynamicComponent describing what should be rendered on the current page. /// public sealed class DynamicComponentComposeResult { /// /// Any content created with the method that should be drawn on the currently rendered page. /// public IElement Content { get; set; } /// /// Set to true if the dynamic component has additional content for the next page. /// Set to false if all content from the dynamic component has been rendered. /// public bool HasMoreContent { get; set; } } /// /// 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. /// /// /// Though dynamic components offer great flexibility, be cautious of potential performance impacts. /// public interface IDynamicComponent { /// /// Method invoked by the library to plan and create new content for each page. /// /// /// Remember, the QuestPDF library can invoke the Compose method more than once for each page and might adjust the state internally. /// /// Context offering additional information (like current page number, entire document size) and the capability to produce dynamic content elements. /// Representation of content that should be placed on the current page. DynamicComponentComposeResult Compose(DynamicContext context); } /// /// 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. /// /// /// Though dynamic components offer great flexibility, be cautious of potential performance impacts. /// /// Structure type representing the internal state of the component. public interface IDynamicComponent : IDynamicComponent where TState : struct { /// /// Represents the component's current state. /// /// /// /// The state should remain read-only. /// Avoid direct state modifications. /// For any alterations, generate a new struct instance and reassign the State property. /// /// Remember, the QuestPDF library can invoke the Compose method more than once for each page and might adjust the state internally. /// TState State { get; set; } } } ================================================ FILE: Source/QuestPDF/Infrastructure/IElement.cs ================================================ namespace QuestPDF.Infrastructure { /// /// Represents an element within the document content, for example:
/// - visual elements (text, image, canvas),
/// - positional elements (padding, aspect ratio),
/// - flow-control elements (page break, conditional displays),
/// - layout elements (table, column),
/// - and other element types (section, hyperlink, content direction). ///
public interface IElement { } } ================================================ FILE: Source/QuestPDF/Infrastructure/IMergedDocument.cs ================================================ using System; using System.Collections.Generic; using System.Linq; namespace QuestPDF.Infrastructure { internal enum MergedDocumentPageNumberStrategy { Original, Continuous, } public sealed class MergedDocument : IDocument { internal IReadOnlyList Documents { get; } internal MergedDocumentPageNumberStrategy PageNumberStrategy { get; private set; } = MergedDocumentPageNumberStrategy.Original; internal DocumentMetadata Metadata { get; private set; } = DocumentMetadata.Default; internal DocumentSettings Settings { get; private set; } = DocumentSettings.Default; internal MergedDocument(IEnumerable documents) { Documents = documents?.ToList() ?? throw new NullReferenceException(nameof(documents)); } public void Compose(IDocumentContainer container) { foreach (var document in Documents) { document.Compose(container); } } public DocumentMetadata GetMetadata() { return Metadata; } public DocumentSettings GetSettings() { return Settings; } /// /// Documents maintain their own page numbers upon merging, without continuity between them. /// As a result, APIs related to page numbers reflect individual documents, not the cumulative count. /// All documents are simply be merged together. /// /// /// Merging a two-page document with a three-page document results in a sequence: 1, 2, 1, 2, 3. /// public MergedDocument UseOriginalPageNumbers() { PageNumberStrategy = MergedDocumentPageNumberStrategy.Original; return this; } /// /// Consolidates the content from every document, creating a continuous seamless one. /// Page number APIs return a consecutive numbering for this unified document. /// /// /// Merging a two-page document with a three-page document results in a sequence: 1, 2, 3, 4, 5. /// public MergedDocument UseContinuousPageNumbers() { PageNumberStrategy = MergedDocumentPageNumberStrategy.Continuous; return this; } public MergedDocument WithMetadata(DocumentMetadata metadata) { Metadata = metadata ?? Metadata; return this; } public MergedDocument WithSettings(DocumentSettings settings) { Settings = settings ?? Settings; return this; } } } ================================================ FILE: Source/QuestPDF/Infrastructure/IPageContext.cs ================================================ using System.Collections.Generic; using System.Numerics; namespace QuestPDF.Infrastructure { internal sealed class DocumentLocation { public int DocumentId { get; set; } public string Name { get; set; } public int PageStart { get; set; } public int PageEnd { get; set; } public int Length => PageEnd - PageStart + 1; } public class PageElementLocation { public string Id { get; set; } public int PageNumber { get; set; } public float Width { get; set; } public float Height { get; set; } public float X { get; set; } public float Y { get; set; } public Matrix4x4 Transform { get; set; } = Matrix4x4.Identity; } internal interface IPageContext { bool IsInitialRenderingPhase { get; } int DocumentLength { get; } int CurrentPage { get; } void SetSectionPage(string name); DocumentLocation? GetLocation(string name); string GetDocumentLocationName(string locationName); void CaptureContentPosition(PageElementLocation location); ICollection GetContentCapturedPositions(string id); } } ================================================ FILE: Source/QuestPDF/Infrastructure/ISemanticAware.cs ================================================ using QuestPDF.Drawing; namespace QuestPDF.Infrastructure; internal interface ISemanticAware { public SemanticTreeManager? SemanticTreeManager { get; set; } } ================================================ FILE: Source/QuestPDF/Infrastructure/IStateful.cs ================================================ namespace QuestPDF.Infrastructure { internal interface IStateful { void ResetState(bool hardReset = true); object GetState(); void SetState(object state); } } ================================================ FILE: Source/QuestPDF/Infrastructure/Image.cs ================================================ using System; using System.Collections.Generic; using System.IO; using QuestPDF.Drawing.Exceptions; using QuestPDF.Helpers; using QuestPDF.Skia; namespace QuestPDF.Infrastructure { internal record GetImageVersionRequest { internal ImageSize Resolution { get; set; } internal ImageCompressionQuality CompressionQuality { get; set; } } /// /// Caches the image in local memory for efficient reuse. /// Optimizes the generation process, especially: /// - For images repeated in a single document to enhance performance and reduce output file size (e.g., an image used as list bullet icon). /// - When an image appears on multiple document types for increased generation performance (e.g., a company logo). /// /// /// This class is thread safe. /// public sealed class Image : IDisposable { static Image() { SkNativeDependencyCompatibilityChecker.Test(); } internal bool IsShared { get; set; } = true; internal SkImage SkImage { get; } internal ImageSize Size { get; } internal LinkedList<(GetImageVersionRequest request, SkImage image)> ScaledImageCache { get; } = new(); internal Image(SkImage image) { SkImage = image; Size = new ImageSize(image.Width, image.Height); } ~Image() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { SkImage?.Dispose(); foreach (var cacheKey in ScaledImageCache) cacheKey.image?.Dispose(); GC.SuppressFinalize(this); } #region Scaling Image internal SkImage GetVersionOfSize(GetImageVersionRequest request) { foreach (var cacheKey in ScaledImageCache) { if (cacheKey.request == request) return cacheKey.image; } var result = SkImage.ResizeAndCompressImage(request.Resolution, request.CompressionQuality); ScaledImageCache.AddLast((request, result)); return result; } #endregion #region public constructors /// /// Loads the image from binary data. /// Learn more /// /// public static Image FromBinaryData(byte[] imageBytes) { using var imageData = SkData.FromBinary(imageBytes); return StaticImageCache.DecodeImage(imageData, isShared: true); } /// /// Loads the image from a file with specified path. /// Learn more /// /// public static Image FromFile(string filePath) { return StaticImageCache.DirectlyLoadFromFile(filePath, isShared: true); } /// /// Loads the image from a stream. /// Learn more /// /// public static Image FromStream(Stream stream) { using var imageData = SkData.FromStream(stream); return StaticImageCache.DecodeImage(imageData, isShared: true); } #endregion } } ================================================ FILE: Source/QuestPDF/Infrastructure/ImageCompressionQuality.cs ================================================ namespace QuestPDF.Infrastructure { public enum ImageCompressionQuality { /// /// JPEG format with target quality set to 100 out of 100 /// Best, /// /// JPEG format with target quality set to 90 out of 100 /// VeryHigh, /// /// JPEG format with target quality set to 75 out of 100 /// High, /// /// JPEG format with target quality set to 50 out of 100 /// Medium, /// /// JPEG format with target quality set to 25 out of 100 /// Low, /// /// JPEG format with target quality set to 10 out of 100 /// VeryLow } } ================================================ FILE: Source/QuestPDF/Infrastructure/ImageFormat.cs ================================================ namespace QuestPDF.Infrastructure { public enum ImageFormat { Jpeg, Png, Webp } } ================================================ FILE: Source/QuestPDF/Infrastructure/ImageGenerationSettings.cs ================================================ namespace QuestPDF.Infrastructure { public sealed class ImageGenerationSettings { /// /// The file format used to encode the image(s). /// public ImageFormat ImageFormat { get; set; } = ImageFormat.Png; /// /// Encoding quality controls the trade-off between size and quality. /// The default value is "high". /// public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.High; /// /// The DPI (pixels-per-inch) at which the document will be rasterized. This parameter controls the resolution of produced images. /// Higher DPI results in superior image quality but may increase the output file size. /// Default value is 288. /// /// /// Consider a document of dimensions 3x4 inches. Using a DPI value of 300, the final image resolution translates to 900x1200 pixels. /// public int RasterDpi { get; set; } = DocumentSettings.DefaultRasterDpi * 4; public static ImageGenerationSettings Default => new ImageGenerationSettings(); } } ================================================ FILE: Source/QuestPDF/Infrastructure/ImageScaling.cs ================================================ namespace QuestPDF.Infrastructure { public enum ImageScaling { FitWidth, FitHeight, FitArea, Resize } } ================================================ FILE: Source/QuestPDF/Infrastructure/ImageSize.cs ================================================ namespace QuestPDF.Infrastructure { public readonly struct ImageSize { public readonly int Width; public readonly int Height; public ImageSize(int width, int height) { Width = width; Height = height; } } } ================================================ FILE: Source/QuestPDF/Infrastructure/LicenseType.cs ================================================ namespace QuestPDF.Infrastructure { /// /// The QuestPDF library is available under a hybrid license favorable to both community and business users. /// For a comprehensive overview, please visit the License and Pricing details webpage. /// public enum LicenseType { /// /// The QuestPDF Evaluation License is intended solely for evaluation before choosing an appropriate license. /// It is not suitable for production use. /// Learn more /// Evaluation, /// /// The QuestPDF Community MIT License is applicable mainly for companies and individuals with less than $1M USD annual gross revenue. /// Learn more /// Community, /// /// The QuestPDF Professional License is applicable for individuals and companies with at most 10 software developers. /// Learn more /// Professional, /// /// The QuestPDF Enterprise License is applicable for individuals and companies with any number of software developers. /// Learn more /// Enterprise } } ================================================ FILE: Source/QuestPDF/Infrastructure/PageContext.cs ================================================ using System; using System.Collections.Generic; using System.Linq; namespace QuestPDF.Infrastructure { internal sealed class PageContext : IPageContext { public bool IsInitialRenderingPhase { get; private set; } = true; public int DocumentLength { get; private set; } private List Locations { get; } = new(); public int CurrentDocumentId { get; private set; } public int CurrentPage { get; private set; } internal void SetDocumentId(int id) { CurrentDocumentId = id; } internal void ProceedToNextRenderingPhase() { IsInitialRenderingPhase = false; CurrentPage = 0; } internal void DecrementPageNumber() { CurrentPage--; } internal void IncrementPageNumber() { CurrentPage++; DocumentLength = Math.Max(DocumentLength, CurrentPage); } public void SetSectionPage(string name) { var location = GetLocation(name); if (location == null) { location = new DocumentLocation { DocumentId = CurrentDocumentId, Name = name, PageStart = CurrentPage, PageEnd = CurrentPage }; Locations.Add(location); } if (location.PageEnd < CurrentPage) location.PageEnd = CurrentPage; } public DocumentLocation? GetLocation(string name) { return Locations.Find(x => x.DocumentId == CurrentDocumentId && x.Name == name); } public string GetDocumentLocationName(string locationName) { return $"{CurrentDocumentId} | {locationName}"; } private List ContentCapturedPositions { get; } = new(); public void CaptureContentPosition(PageElementLocation location) { ContentCapturedPositions.Add(location); } public ICollection GetContentCapturedPositions(string id) { return ContentCapturedPositions.Where(x => x.Id == id).ToList(); } } } ================================================ FILE: Source/QuestPDF/Infrastructure/Position.cs ================================================ using System; namespace QuestPDF.Infrastructure { internal readonly struct Position { public readonly float X; public readonly float Y; public static Position Zero => new Position(0, 0); public Position(float x, float y) { X = x; Y = y; } public Position Reverse() { return new Position(-X, -Y); } public static bool Equal(Position first, Position second) { if (Math.Abs(first.X - second.X) > Size.Epsilon) return false; if (Math.Abs(first.Y - second.Y) > Size.Epsilon) return false; return true; } public override string ToString() => $"(Left: {X:N3}, Top: {Y:N3})"; } } ================================================ FILE: Source/QuestPDF/Infrastructure/Size.cs ================================================ using System; namespace QuestPDF.Infrastructure { public readonly struct Size { public const float Epsilon = 0.01f; public const float Infinity = 14_400; public readonly float Width; public readonly float Height; public static Size Zero { get; } = new Size(0, 0); public static Size Max { get; } = new Size(Infinity, Infinity); public Size(float width, float height) { Width = width; Height = height; } internal static bool Equal(Size first, Size second) { if (Math.Abs(first.Width - second.Width) > Size.Epsilon) return false; if (Math.Abs(first.Height - second.Height) > Size.Epsilon) return false; return true; } public override string ToString() => $"(Width: {Width:N3}, Height: {Height:N3})"; } } ================================================ FILE: Source/QuestPDF/Infrastructure/SourceCodePath.cs ================================================ using System.Diagnostics; using System.Linq; using QuestPDF.Companion; namespace QuestPDF.Infrastructure; internal readonly struct SourceCodePath(StackFrame frame) { public readonly string FilePath = frame.GetFileName() ?? string.Empty; public readonly int LineNumber = frame.GetFileLineNumber(); internal static SourceCodePath? CreateFromCurrentStackTrace() { #if NET6_0_OR_GREATER if (!CompanionService.IsCompanionAttached) return null; // for dotnet 6, 7, 8: // - after hot-reload, the stack trace does not contain correct source code path // - the operation of collecting stack trace slows down generation process significantly // TODO: revise for dotnet 9 if (CompanionService.IsDocumentHotReloaded) return null; #else return null; #endif var stackTrace = new StackTrace(true); var frame = stackTrace.GetFrames().FirstOrDefault(x => x.HasSource() && x.HasMethod()); if (frame == null) return null; return new SourceCodePath(frame); } } ================================================ FILE: Source/QuestPDF/Infrastructure/StaticImageCache.cs ================================================ using System.Collections.Concurrent; using System.IO; using System.Linq; using QuestPDF.Drawing.Exceptions; using QuestPDF.Skia; namespace QuestPDF.Infrastructure; static class StaticImageCache { private static bool CacheIsEnabled { get; set; } = true; private static ConcurrentDictionary Items { get; set; } = new(); private const int MaxCacheSize = 25_000_000; private const int MaxItemSize = 1_000_000; public static Image LoadFromCache(string filePath) { // check fallback path filePath = AdjustPath(filePath); // check if the image is already in cache if (Items.TryGetValue(filePath, out var cacheItem)) return cacheItem; // check file size var fileInfo = new FileInfo(filePath); if (fileInfo.Length > MaxItemSize) return DirectlyLoadFromFile(filePath, false); // if cache is larger than expected, the usage might be different from loading static images if (!CacheIsEnabled) return DirectlyLoadFromFile(filePath, false); // create new cache item and add it to the cache var image = DirectlyLoadFromFile(filePath, true); Items.TryAdd(filePath, image); // check cache size CacheIsEnabled = Items.Values.Sum(x => x.SkImage.EncodedDataSize) < MaxCacheSize; // return cached value return image; } public static string AdjustPath(string filePath) { if (File.Exists(filePath)) return filePath; if (Path.IsPathRooted(filePath)) throw new DocumentComposeException($"Cannot load an image under the provided absolute path, file not found: {filePath}"); var fallbackPath = Path.Combine(Helpers.Helpers.ApplicationFilesPath, filePath); if (!File.Exists(fallbackPath)) throw new DocumentComposeException($"Cannot load an image under the provided relative path, file not found: {filePath}"); return fallbackPath; } public static Image DirectlyLoadFromFile(string filePath, bool isShared) { filePath = AdjustPath(filePath); using var imageData = SkData.FromFile(filePath); return DecodeImage(imageData, isShared); } public static Image DecodeImage(SkData imageData, bool isShared) { try { var skImage = SkImage.FromData(imageData); var image = new Image(skImage); image.IsShared = isShared; return image; } catch { throw new DocumentComposeException("Cannot decode the provided image."); } } } ================================================ FILE: Source/QuestPDF/Infrastructure/SvgImage.cs ================================================ using System; using System.IO; using QuestPDF.Drawing; using QuestPDF.Drawing.Exceptions; using QuestPDF.Skia; namespace QuestPDF.Infrastructure; /// /// Caches the SVG image in local memory for efficient reuse. /// /// /// This class is thread safe. /// public sealed class SvgImage : IDisposable { internal SkSvgImage SkSvgImage { get; } internal bool IsShared { get; set; } = true; private SvgImage(string content) { SkSvgImage = new SkSvgImage(content, SkResourceProvider.CurrentResourceProvider, FontManager.CurrentFontManager); } ~SvgImage() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { SkSvgImage?.Dispose(); GC.SuppressFinalize(this); } /// /// Loads the SVG image from a file with specified path. /// Learn more /// /// public static SvgImage FromFile(string filePath) { if (!File.Exists(filePath)) { var fallbackPath = Path.Combine(Helpers.Helpers.ApplicationFilesPath, filePath); if (!File.Exists(fallbackPath)) throw new DocumentComposeException($"Cannot load an SVG image under the provided path, file not found: ${filePath}"); filePath = fallbackPath; } var svg = File.ReadAllText(filePath); return new SvgImage(svg); } /// /// Loads the SVG image from a stream. /// Learn more /// /// public static SvgImage FromText(string svg) { return new SvgImage(svg); } } ================================================ FILE: Source/QuestPDF/Infrastructure/TextDirection.cs ================================================ namespace QuestPDF.Infrastructure { internal enum TextDirection { Auto, LeftToRight, RightToLeft } } ================================================ FILE: Source/QuestPDF/Infrastructure/TextHorizontalAlignment.cs ================================================ namespace QuestPDF.Infrastructure; public enum TextHorizontalAlignment { Left, Center, Right, Justify, Start, End } ================================================ FILE: Source/QuestPDF/Infrastructure/TextInjectedElementAlignment.cs ================================================ namespace QuestPDF.Infrastructure; public enum TextInjectedElementAlignment { /// /// Aligns the bottom edge of the injected element with the text baseline. The injected element sits on top of the baseline. /// AboveBaseline, /// /// Aligns the top edge of the injected element with the text baseline. The injected element hangs below the baseline. /// BelowBaseline, /// /// Aligns the top edge of the injected element with the top edge of the font. If the injected element is very tall, the extra space will hang from the top and extend through the bottom of the line. /// Top, /// /// Aligns the bottom edge of the injected element with the top edge of the font. If the injected element is very tall, the extra space will rise from the bottom and extend through the top of the line. /// Bottom, /// /// Aligns the middle of the injected element with the middle of the text. If the injected element is very tall, the extra space will grow equally from the top and bottom of the line. /// Middle } ================================================ FILE: Source/QuestPDF/Infrastructure/TextStyle.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using QuestPDF.Helpers; using QuestPDF.Skia; using QuestPDF.Skia.Text; using TextStyleFontFeature = (string Name, bool Enabled); namespace QuestPDF.Infrastructure { public record TextStyle { internal const float NormalLineHeightCalculatedFromFontMetrics = 0; internal int Id { get; set; } internal Color? Color { get; set; } internal Color? BackgroundColor { get; set; } internal Color? DecorationColor { get; set; } internal string[]? FontFamilies { get; set; } internal TextStyleFontFeature[]? FontFeatures { get; set; } internal float? Size { get; set; } internal float? LineHeight { get; set; } internal float? LetterSpacing { get; set; } internal float? WordSpacing { get; set; } internal FontWeight? FontWeight { get; set; } internal FontPosition? FontPosition { get; set; } internal bool? IsItalic { get; set; } internal bool? HasStrikethrough { get; set; } internal bool? HasUnderline { get; set; } internal bool? HasOverline { get; set; } internal TextStyleConfiguration.TextDecorationStyle? DecorationStyle { get; set; } internal float? DecorationThickness { get; set; } internal TextDirection? Direction { get; set; } ~TextStyle() { // TextStyle is meant to be an object spanning the entire application lifetime // It does not require the IDisposable pattern SkTextStyleCache?.Dispose(); } public static TextStyle Default { get; } = new() { Id = 0 }; internal static TextStyle LibraryDefault { get; } = new() { Id = 1, Color = Colors.Black, BackgroundColor = Colors.Transparent, DecorationColor = Colors.Black, FontFamilies = [ Fonts.Lato ], FontFeatures = [], Size = 12, LineHeight = NormalLineHeightCalculatedFromFontMetrics, LetterSpacing = 0, WordSpacing = 0f, FontWeight = Infrastructure.FontWeight.Normal, FontPosition = Infrastructure.FontPosition.Normal, IsItalic = false, HasStrikethrough = false, HasUnderline = false, HasOverline = false, DecorationStyle = TextStyleConfiguration.TextDecorationStyle.Solid, DecorationThickness = 1f, Direction = TextDirection.Auto }; internal static TextStyle ParagraphSpacing { get; } = LibraryDefault with { Id = 2, Size = 0, LineHeight = 1 }; private volatile SkTextStyle? SkTextStyleCache; private readonly object SkTextStyleCacheLock = new(); internal SkTextStyle GetSkTextStyle() { if (SkTextStyleCache != null) return SkTextStyleCache; lock (SkTextStyleCacheLock) { if (SkTextStyleCache != null) return SkTextStyleCache; var temp = CreateSkTextStyle(); SkTextStyleCache = temp; return temp; } } private SkTextStyle CreateSkTextStyle() { var fontFamilyTexts = FontFamilies.Select(x => new SkText(x)).ToList(); var result = new SkTextStyle(new TextStyleConfiguration { FontSize = CalculateTargetFontSize(), FontWeight = (TextStyleConfiguration.FontWeights?)FontWeight ?? TextStyleConfiguration.FontWeights.Normal, IsItalic = IsItalic ?? false, FontFamilies = GetFontFamilyPointers(fontFamilyTexts), FontFeatures = GetFontFeatures(), ForegroundColor = Color ?? Colors.Black, BackgroundColor = BackgroundColor ?? Colors.Transparent, DecorationColor = DecorationColor ?? Colors.Black, DecorationType = CreateDecoration(), DecorationMode = TextStyleConfiguration.TextDecorationMode.Through, DecorationStyle = DecorationStyle ?? TextStyleConfiguration.TextDecorationStyle.Solid, DecorationThickness = DecorationThickness ?? 1, LineHeight = LineHeight ?? NormalLineHeightCalculatedFromFontMetrics, LetterSpacing = (LetterSpacing ?? 0) * (Size ?? 1), WordSpacing = (WordSpacing ?? 0) * (Size ?? 1), BaselineOffset = CalculateBaselineOffset(), }); fontFamilyTexts.ForEach(x => x.Dispose()); return result; IntPtr[] GetFontFamilyPointers(IList texts) { var result = new IntPtr[TextStyleConfiguration.FONT_FAMILIES_LENGTH]; for (var i = 0; i < Math.Min(result.Length, texts.Count); i++) result[i] = texts[i].Instance; return result; } TextStyleConfiguration.FontFeature[] GetFontFeatures(params (string name, int value)[] features) { var result = new TextStyleConfiguration.FontFeature[TextStyleConfiguration.FONT_FEATURES_LENGTH]; foreach (var (feature, index) in FontFeatures.Take(TextStyleConfiguration.FONT_FEATURES_LENGTH).Select((x, i) => (x, i))) { result[index].Name = feature.Name; result[index].Value = feature.Enabled ? 1 : 0; } return result; } TextStyleConfiguration.TextDecoration CreateDecoration() { var result = TextStyleConfiguration.TextDecoration.NoDecoration; if (HasUnderline == true) result |= TextStyleConfiguration.TextDecoration.Underline; if (HasStrikethrough == true) result |= TextStyleConfiguration.TextDecoration.LineThrough; if (HasOverline == true) result |= TextStyleConfiguration.TextDecoration.Overline; return result; } float CalculateTargetFontSize() { var fontSize = Size ?? 0; if (FontPosition is Infrastructure.FontPosition.Subscript or Infrastructure.FontPosition.Superscript) return fontSize * 0.6f; return fontSize; } float CalculateBaselineOffset() { if (FontPosition == Infrastructure.FontPosition.Subscript) return Size.Value * 0.25f; if (FontPosition == Infrastructure.FontPosition.Superscript) return -Size.Value * 0.35f; return 0; } } } } ================================================ FILE: Source/QuestPDF/Infrastructure/TextStyleManager.cs ================================================ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using TextStyleFontFeature = (string Name, bool Enabled); namespace QuestPDF.Infrastructure { internal enum TextStyleProperty { Color, BackgroundColor, DecorationColor, FontFamilies, FontFeatures, Size, LineHeight, LetterSpacing, WordSpacing, FontWeight, FontPosition, IsItalic, HasStrikethrough, HasUnderline, HasOverline, DecorationStyle, DecorationThickness, Direction } // C# does not have proper equality members for arrays // this struct is a wrapper that allows to use an array as part of dictionary key internal struct ArrayContainer { public T[] Items { get; } public ArrayContainer(object array) { Items = (array as T[]) ?? Array.Empty(); } public bool Equals(ArrayContainer other) { return Items.SequenceEqual(other.Items); } public override bool Equals(object obj) { return obj is ArrayContainer other && Equals(other); } public override int GetHashCode() { if (Items.Length == 1) return Items[0].GetHashCode(); unchecked { var hash = 19; foreach (var item in Items) hash = hash * 31 + item.GetHashCode(); return hash; } } } internal static class TextStyleManager { private static readonly List TextStyles = new() { TextStyle.Default, TextStyle.LibraryDefault, TextStyle.ParagraphSpacing }; private static readonly ConcurrentDictionary<(int originId, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new(); private static readonly ConcurrentDictionary<(int originId, int parentId), TextStyle> TextStyleApplyInheritedCache = new(); private static readonly ConcurrentDictionary TextStyleApplyGlobalCache = new(); private static readonly ConcurrentDictionary<(int originId, int parentId), TextStyle> TextStyleOverrideCache = new(); private static readonly object MutationLock = new(); public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value) { if (property is TextStyleProperty.FontFamilies) value = new ArrayContainer(value); if (property is TextStyleProperty.FontFeatures) value = new ArrayContainer(value); var cacheKey = (origin.Id, property, value); return TextStyleMutateCache.GetOrAdd(cacheKey, x => { var newValue = x.value; if (x.value is ArrayContainer fontFamilies) newValue = fontFamilies.Items; if (x.value is ArrayContainer fontFeatures) newValue = fontFeatures.Items; return MutateStyle(TextStyles[x.originId], x.property, newValue, overrideValue: true); }); } private static TextStyle MutateStyle(this TextStyle origin, TextStyleProperty targetProperty, object? newValue, bool overrideValue) { if (targetProperty == TextStyleProperty.FontFamilies) return MutateFontFamily(origin, newValue as string[], overrideValue); if (targetProperty == TextStyleProperty.FontFeatures) return MutateFontFeatures(origin, newValue as TextStyleFontFeature[], overrideValue); lock (MutationLock) { if (overrideValue && newValue is null) return origin; var property = typeof(TextStyle).GetProperty(targetProperty.ToString(), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Debug.Assert(property != null); var oldValue = property.GetValue(origin); if (!overrideValue && oldValue is not null) return origin; if (oldValue == newValue) return origin; var newIndex = TextStyles.Count; var newTextStyle = origin with { Id = newIndex }; property.SetValue(newTextStyle, newValue); TextStyles.Add(newTextStyle); return newTextStyle; } } private static TextStyle MutateFontFamily(this TextStyle origin, string[]? newValue, bool overrideValue) { lock (MutationLock) { if (overrideValue && newValue is null) return origin; newValue ??= Array.Empty(); var oldValue = origin.FontFamilies ?? Array.Empty(); if (origin.FontFamilies?.SequenceEqual(newValue) == true) return origin; var newIndex = TextStyles.Count; var newTextStyle = origin with { Id = newIndex }; newTextStyle.FontFamilies = overrideValue ? newValue : oldValue.Concat(newValue).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToArray(); TextStyles.Add(newTextStyle); return newTextStyle; } } private static TextStyle MutateFontFeatures(this TextStyle origin, TextStyleFontFeature[]? newValue, bool overrideValue) { lock (MutationLock) { if (overrideValue && newValue is null) return origin; newValue ??= []; var oldValue = origin.FontFeatures ?? []; if (origin.FontFeatures?.SequenceEqual(newValue) == true) return origin; var extendedSet = overrideValue ? newValue.Concat(oldValue) : oldValue.Concat(newValue); var newIndex = TextStyles.Count; var newTextStyle = origin with { Id = newIndex }; newTextStyle.FontFeatures = extendedSet .GroupBy(x => x.Name) .Select(x => x.First()) .ToArray(); TextStyles.Add(newTextStyle); return newTextStyle; } } internal static TextStyle ApplyInheritedStyle(this TextStyle style, TextStyle parent) { return TextStyleApplyInheritedCache.GetOrAdd((style.Id, parent.Id), key => ApplyStyleProperties(key.originId, key.parentId, overrideStyle: false)); } internal static TextStyle ApplyGlobalStyle(this TextStyle style) { return TextStyleApplyGlobalCache.GetOrAdd(style.Id, key => ApplyStyleProperties(key, TextStyle.LibraryDefault.Id, overrideStyle: false)); } internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent) { return TextStyleOverrideCache.GetOrAdd((style.Id, parent.Id), key => ApplyStyleProperties(key.originId, key.parentId, overrideStyle: true)); } private static TextStyle ApplyStyleProperties(int styleId, int parentId, bool overrideStyle) { var style = TextStyles[styleId]; var parent = TextStyles[parentId]; return Enum .GetValues(typeof(TextStyleProperty)) .Cast() .Aggregate(style, (mutableStyle, nextProperty) => { var getParentProperty = typeof(TextStyle).GetProperty(nextProperty.ToString(), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Debug.Assert(getParentProperty != null); var newValue = getParentProperty.GetValue(parent); return mutableStyle.MutateStyle(nextProperty, newValue, overrideStyle); }); } internal static TextStyle GetTextStyle(int id) { return TextStyles[id]; } } } ================================================ FILE: Source/QuestPDF/Infrastructure/Unit.cs ================================================ using System; using static QuestPDF.Infrastructure.Unit; namespace QuestPDF.Infrastructure { public enum Unit { /// /// Point is a standard unit for all PDF documents. /// 72 points equal 1 inch /// Point, Meter, Centimetre, Millimetre, Feet, Inch, /// /// 1/1000th of inch /// Mil } internal static class UnitExtensions { private const float InchToCentimetre = 2.54f; private const float InchToPoints = 72; public static float ToPoints(this float value, Unit unit) { return value * GetConversionFactor(); float GetConversionFactor() { return unit switch { Point => 1, Meter => 100 / InchToCentimetre * InchToPoints, Centimetre => 1 / InchToCentimetre * InchToPoints, Millimetre => 0.1f / InchToCentimetre * InchToPoints, Feet => 12 * InchToPoints, Inch => InchToPoints, Mil => InchToPoints / 1000f, _ => throw new ArgumentOutOfRangeException(nameof(unit), unit, null) }; } } } } ================================================ FILE: Source/QuestPDF/Infrastructure/VerticalAlignment.cs ================================================ namespace QuestPDF.Infrastructure { public enum VerticalAlignment { Top, Middle, Bottom } } ================================================ FILE: Source/QuestPDF/LatoFont/OFL.txt ================================================ Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: Source/QuestPDF/Qpdf/JobConfiguration.cs ================================================ using System.Collections.Generic; namespace QuestPDF.Qpdf; using Name = SimpleJsonPropertyNameAttribute; sealed class JobConfiguration { [Name("inputFile")] public string InputFile { get; set; } [Name("password")] public string? Password { get; set; } [Name("outputFile")] public string OutputFile { get; set; } [Name("pages")] public ICollection? Pages { get; set; } [Name("overlay")] public ICollection? Overlay { get; set; } [Name("underlay")] public ICollection? Underlay { get; set; } [Name("extendMetadata")] public string? ExtendMetadata { get; set; } [Name("addAttachment")] public ICollection? AddAttachment { get; set; } [Name("encrypt")] public EncryptionSettings? Encrypt { get; set; } [Name("allowWeakCrypto")] public string AllowWeakCrypto { get; set; } = string.Empty; [Name("decrypt")] public string? Decrypt { get; set; } [Name("removeRestrictions")] public string? RemoveRestrictions { get; set; } [Name("linearize")] public string? Linearize { get; set; } [Name("newlineBeforeEndstream")] public string? NewlineBeforeEndstream { get; set; } = string.Empty; [Name("keepFilesOpen")] public string? KeepFilesOpen { get; set; } = "n"; internal sealed class PageConfiguration { [Name("file")] public string File { get; set; } [Name("range")] public string Range { get; set; } } internal sealed class LayerConfiguration { [Name("file")] public string File { get; set; } [Name("to")] public string? To { get; set; } [Name("from")] public string? From { get; set; } [Name("repeat")] public string? Repeat { get; set; } } public sealed class AddDocumentAttachment { [Name("key")] public string Key { get; set; } [Name("file")] public string File { get; set; } [Name("filename")] public string? FileName { get; set; } [Name("creationdate")] public string? CreationDate { get; set; } [Name("moddate")] public string? ModificationDate { get; set; } [Name("mimetype")] public string? MimeType { get; set; } [Name("description")] public string? Description { get; set; } [Name("replace")] public string? Replace { get; set; } [Name("relationship")] public string? Relationship { get; set; } } public sealed class EncryptionSettings { [Name("userPassword")] public string? UserPassword { get; set; } [Name("ownerPassword")] public string OwnerPassword { get; set; } [Name("40bit")] public Encryption40Bit? Options40Bit { get; set; } [Name("128bit")] public Encryption128Bit? Options128Bit { get; set; } [Name("256bit")] public Encryption256Bit? Options256Bit { get; set; } } public sealed class Encryption40Bit { [Name("annotate")] public string Annotate { get; set; } [Name("extract")] public string Extract { get; set; } [Name("modify")] public string Modify { get; set; } [Name("print")] public string Print { get; set; } } public sealed class Encryption128Bit { [Name("annotate")] public string Annotate { get; set; } [Name("assemble")] public string Assemble { get; set; } [Name("extract")] public string Extract { get; set; } [Name("form")] public string Form { get; set; } [Name("print")] public string? Print { get; set; } [Name("useAes")] public string? UseAES { get; set; } = "y"; [Name("cleartextMetadata")] public string? CleartextMetadata { get; set; } } public sealed class Encryption256Bit { [Name("annotate")] public string Annotate { get; set; } [Name("assemble")] public string Assemble { get; set; } [Name("extract")] public string Extract { get; set; } [Name("form")] public string Form { get; set; } [Name("print")] public string? Print { get; set; } [Name("cleartextMetadata")] public string? CleartextMetadata { get; set; } } } ================================================ FILE: Source/QuestPDF/Qpdf/MimeHelper.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; namespace QuestPDF.Qpdf; static class MimeHelper { public static readonly IReadOnlyDictionary FileExtensionToMimeConversionTable = LoadMimeMapping(); private static IReadOnlyDictionary LoadMimeMapping() { using var stream = Assembly .GetExecutingAssembly() .GetManifestResourceStream("QuestPDF.Resources.MimeTypes.csv"); using var streamReader = new StreamReader(stream); var text = streamReader.ReadToEnd(); return text .Split('\n') .Select(x => x.Split(',')) .ToDictionary(x => x.First(), x => x.Last()); } } ================================================ FILE: Source/QuestPDF/Qpdf/QpdfAPI.cs ================================================ using System; using System.Runtime.InteropServices; using System.Text; using QuestPDF.Skia; namespace QuestPDF.Qpdf; static class QpdfAPI { public static string? GetQpdfVersion() { var ptr = API.qpdf_get_qpdf_version(); return Marshal.PtrToStringAnsi(ptr); } public static void ExecuteJob(string jobJson) { QpdfNativeDependencyCompatibilityChecker.Test(); // create StringBuilder that will store the error message var error = new StringBuilder(); var errorHandle = GCHandle.Alloc(error); var errorPtr = GCHandle.ToIntPtr(errorHandle); // create logger var logger = API.qpdflogger_create(); API.qpdflogger_set_error(logger, 4, LoggingCallbackPointer, errorPtr); // 4 = custom logger // perform the job var jobHandle = API.qpdfjob_init(); API.qpdfjob_set_logger(jobHandle, logger); API.qpdfjob_initialize_from_json(jobHandle, jobJson); var jobResultId = API.qpdfjob_run(jobHandle); API.qpdfjob_cleanup(jobHandle); // logger cleanup API.qpdflogger_cleanup(logger); // check errors var isError = jobResultId is 2; // 0 = success, 1 = undefined, 2 = error, 3 = warning var errorMessage = error.ToString(); errorHandle.Free(); if (isError) throw new Exception($"QuestPDF could not perform document operation:\n\n{errorMessage}"); } #region Logging private static int LoggingCallback(IntPtr data, int length, IntPtr udata) { var bytes = new byte[length]; Marshal.Copy(data, bytes, 0, length); var handle = GCHandle.FromIntPtr(udata); var stringBuilder = (StringBuilder)handle.Target; stringBuilder?.Append(Encoding.ASCII.GetString(bytes)); return 0; } private delegate int CallbackDelegate(IntPtr data, int length, IntPtr udata); private static readonly CallbackDelegate LoggingCallbackDelegate = LoggingCallback; private static readonly IntPtr LoggingCallbackPointer = Marshal.GetFunctionPointerForDelegate(LoggingCallbackDelegate); #endregion private static class API { const string LibraryName = "qpdf"; /* GENERAL */ [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr qpdf_get_qpdf_version(); /* JOBS */ [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr qpdfjob_init(); [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void qpdfjob_cleanup(IntPtr jobHandle); [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern int qpdfjob_initialize_from_json(IntPtr jobHandle, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string json); [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern int qpdfjob_run(IntPtr jobHandle); /* LOGGING */ [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = nameof(qpdflogger_create))] public static extern IntPtr qpdflogger_create(); [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = nameof(qpdflogger_cleanup))] public static extern void qpdflogger_cleanup(IntPtr loggerHandle); [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = nameof(qpdflogger_set_error))] public static extern void qpdflogger_set_error(IntPtr loggerHandle, int destination, IntPtr callBackHandler, IntPtr udata); [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = nameof(qpdfjob_set_logger))] public static extern void qpdfjob_set_logger(IntPtr jobHandle, IntPtr loggerHandle); } } ================================================ FILE: Source/QuestPDF/Qpdf/QpdfNativeDependencyCompatibilityChecker.cs ================================================ using System; using QuestPDF.Helpers; namespace QuestPDF.Qpdf; internal static class QpdfNativeDependencyCompatibilityChecker { private static NativeDependencyCompatibilityChecker Instance { get; } = new() { ExecuteNativeCode = ExecuteNativeCode, ExceptionHint = GetHint }; public static void Test() { Instance.Test(); } private static void ExecuteNativeCode() { var qpdfVersion = QpdfAPI.GetQpdfVersion(); if (string.IsNullOrEmpty(qpdfVersion)) throw new Exception(); } private static string GetHint() { return $"Please do NOT install the qpdf package."; } } ================================================ FILE: Source/QuestPDF/Qpdf/SimpleJsonSerializer.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace QuestPDF.Qpdf; sealed class SimpleJsonPropertyNameAttribute(string name) : Attribute { public string Name { get; } = name; } /// /// Never use in performance critical scenarios! /// static class SimpleJsonSerializer { public static string Serialize(object obj) { if (obj == null) return "null"; var type = obj.GetType(); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); var stringBuilder = new StringBuilder(); stringBuilder.Append('{'); foreach (var property in properties) { var value = property.GetValue(obj); if (value == default) continue; var name = property.GetCustomAttribute().Name; stringBuilder.Append($"\"{name}\": {SerializeValue(value)}"); stringBuilder.Append(", "); } if (properties.Length > 1) stringBuilder.Length -= 2; stringBuilder.Append('}'); return stringBuilder.ToString(); } private static string SerializeValue(object value) { if (value == null) return "null"; if (value is string text) return $"\"{EscapeStringForJson(text)}\""; if (value is bool) return value.ToString().ToLower(); if (value is IEnumerable enumerable) { var stringBuilder = new StringBuilder(); stringBuilder.Append('['); foreach (var item in enumerable) stringBuilder.Append($"{SerializeValue(item)}, "); // remove trailing comma and space if (enumerable.Any()) stringBuilder.Length -= 2; stringBuilder.Append(']'); return stringBuilder.ToString(); } if (!value.GetType().IsPrimitive) return Serialize(value); return value.ToString(); } private static string EscapeStringForJson(string input) { if (string.IsNullOrEmpty(input)) return input; var builder = new StringBuilder(input.Length); foreach (char c in input) { if (c == '\\') builder.Append("\\\\"); else if (c == '"') builder.Append("\\\""); else if (c == '\b') builder.Append("\\b"); else if (c == '\f') builder.Append("\\f"); else if (c == '\n') builder.Append("\\n"); else if (c == '\r') builder.Append("\\r"); else if (c == '\t') builder.Append("\\t"); else builder.Append(c); } return builder.ToString(); } } ================================================ FILE: Source/QuestPDF/QuestPDF.csproj ================================================  MarcinZiabek CodeFlint QuestPDF 2026.2.4 QuestPDF is a modern library for PDF document generation. Its fluent C# API lets you design complex layouts with clean, readable code. Create invoices, reports, and documents using a flexible, component-based approach. $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt")) 13 true true Logo.png https://www.questpdf.com/logo.webp https://www.questpdf.com/ PackageLicense.md True README.md https://github.com/QuestPDF/library.git git Marcin Ziąbek, QuestPDF contributors pdf generation creation export merge edit html enable net10.0;net8.0;net6.0;netstandard2.0 true snupkg True true true true true NugetStrongNameSigningKeyForQuestPDF.snk true %(RecursiveDir)\%(Filename)%(Extension) PreserveNewest PreserveNewest true LatoFont PreserveNewest PreserveNewest true runtimes\%(RecursiveDir)%(Filename)%(Extension) runtimes\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest PreserveNewest true build\%(RecursiveDir)%(Filename)%(Extension);buildTransitive\%(RecursiveDir)%(Filename)%(Extension) ================================================ FILE: Source/QuestPDF/Resources/Contributors.md ================================================ The QuestPDF library is a community-driven project. We are truly thankful for everyone who has helped improve it, whether by contributing code or through other valuable means, such as offering assistance to others, participating in discussions, and testing. Your efforts are greatly appreciated and play a key role in our ongoing development. QuestPDF Contributors: - MarcinZiabek - girlpunk - bennetbo - AntonyCorbett - Lehonti - knoxyz - maartenba - loxsmoke - jnyrup - wieslawsoltes - thomasstevens89 - sclarke81 - simusr2 - danielchalmers - fredericoregateiro - emanueleguastella - CollinAlpert - rstm-sf - warrantyvoids - SvizelPritula - avobelk - ceee - tinohager - schulz3000 - ruyut - marcmognol - donmurta - jcl-aadlab - lmingle - JeremyVm - ebarnard - MercinaM - rima1098 - JerryZingg You can always find the most up-to-date list at: https://github.com/QuestPDF/QuestPDF/graphs/contributors QuestPDF.Documentation Contributors: - MarcinZiabek - AntonyCorbett - Thorinos - jerone - enricobenedos - Ducki - jnyrup - Scarso327 - jz5 - jordansrowles - Tvde1 - pablopioli You can always find the most up-to-date list at: https://github.com/QuestPDF/QuestPDF-Documentation/graphs/contributors If you notice that our list is out of date or if someone should be added, please let us know by emailing us at contact@questpdf.com. We aim to keep it accurate and will make updates as quickly as we can. Thank you for your help! ================================================ FILE: Source/QuestPDF/Resources/Description.md ================================================ ## QuestPDF - 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. [![GitHub Stars and Stargazers](https://img.shields.io/github/stars/QuestPDF/QuestPDF?style=for-the-badge&label=GitHub%20Stars&logo=github&color=FFEB3B&logoColor=white)](https://github.com/QuestPDF/QuestPDF) [![Nuget package download](https://img.shields.io/nuget/dt/QuestPDF?style=for-the-badge&label=NuGet%20downloads&logo=nuget&color=0277BD&logoColor=white)](https://www.nuget.org/packages/QuestPDF/) [![QuestPDF License](https://img.shields.io/badge/LICENSE-Community%20and%20commercial-2E7D32?style=for-the-badge&logo=googledocs&logoColor=white)](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](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: ![Preview of a PDF document showing the Hello World example](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/f6b28c965e26fe43630316f589339db465c8197e/docs/public/nuget/hello-world.png) [![Quick Start Tutorial](https://img.shields.io/badge/read-tutorial-0288D1?style=for-the-badge)](https://www.questpdf.com/quick-start.html) > 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/) ## 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, [![Explore All QuestPDF Features](https://img.shields.io/badge/explore%20all%20features-0288D1?style=for-the-badge)](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 ![Screenshot showing the QuestPDF Companion App](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/f6b28c965e26fe43630316f589339db465c8197e/docs/public/nuget/companion-light.png) [![Learn about QuestPDF Companion App](https://img.shields.io/badge/learn%20more-0288D1?style=for-the-badge)]([https://www.questpdf.com/companion/features.html](https://www.questpdf.com/companion/usage.html)) [![Learn about QuestPDF Companion App](https://img.shields.io/badge/features-666666?style=for-the-badge)](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"); ``` [![Learn Document Operation API](https://img.shields.io/badge/learn%20more-0288D1?style=for-the-badge)](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 > The library is free for individuals, non-profits, all FOSS projects, and organizations under $1M in annual revenue. [![QuestPDF Pricing](https://img.shields.io/badge/view%20pricing-388E3C?style=for-the-badge)](https://www.questpdf.com/license.html) [![QuestPDF License Terms](https://img.shields.io/badge/license%20terms-666666?style=for-the-badge)](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 ![Preview of a PDF document being an output of the Real-World Invoice tutorial](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/f6b28c965e26fe43630316f589339db465c8197e/docs/public/nuget/invoice.jpg) [![Read Real-world Invoice Tutorial](https://img.shields.io/badge/read%20tutorial-0288D1?style=for-the-badge)](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 [![Nick Chapsas The Easiest Way to Create PDFs in .NET](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/f6b28c965e26fe43630316f589339db465c8197e/docs/public/nuget/youtube-nick-chapsas.jpg)](https://www.youtube.com/watch?v=_M0IgtGWnvE) ### JetBrains: OSS Power-Ups: QuestPDF [![JetBrains OSS Power-Ups: QuestPDF](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/f6b28c965e26fe43630316f589339db465c8197e/docs/public/nuget/youtube-jetbrains.jpg)](https://www.youtube.com/watch?v=-iYvZvpLX0g) ================================================ FILE: Source/QuestPDF/Resources/Documentation.xml ================================================ Color in a valid format (like #FF8800 for orange) or from the predefined set (like Colors.Red.Lighten1). The text key corresponding to the value used when defining the Section element. The URL of the webpage to which the user will be redirected. Although this adjustment modifies the space available to its inner content, some elements might use their own strategies to fill that space. For example, an Image with the setting may retain its size, but its quality could vary based on the DPI setting. In contrast, text will not only appear smaller or bigger; but also a different number of words may fit each line. The scaling factor. Values greater than one enlarge the content, while values less than one reduce it. Please note that there is a significant difference between image resolution (number of pixels vertically and horizontally) and its physical size described in points. Therefore, the resolution of an image is not used for determining its physical size on the document. Multiple strategies exist for the Image element to determine its final size. By default, the image fills all the available width. This behavior can be customized using options within the descriptor class. Images are automatically resized and compressed based on the descriptor's setup. It defaults to global document's settings. Supported formats: PNG, JPEG and WEBP. When enabled, the library does not resize the image to achieve the target DPI, nor compress it with target image quality. Specifies the DPI (dots-per-inch) for rasterizing images, which is a measure of an image's resolution that indicates how many individual dots (or pixels) fit into one inch of a printed image. Higher DPI values result in greater detail and sharpness, making images appear clearer when printed, while lower DPI values can cause images to look blurry or pixelated. The target resolution is computed by multiplying the DPI with the physical image size on the document. Default DPI value is 288 DPI. If the image has lower resolution that the one calculated from the DPI setting, it will NOT be rescaled. Consider an image of dimensions 3x4 inches. Using a DPI value of 300, the final resolution translates to 900x1200 pixels. Controls the balance between an image's file size and its visual fidelity during compression. Higher quality values preserve more detail with larger file sizes, while lower values reduce file size at the cost of potential image degradation, such as blurriness or artifacts. Opaque images are JPEG-encoded based on this setting, while images with an alpha channel default to PNG format, disregarding this option. Default is set to "high quality". Descriptor allowing adjustments to image attributes, such as scaling behavior, compression quality, and target DPI. Multiple strategies exist for the Image element to determine its final size. By default, the image fills all the available width. This behavior can be customized using options within the descriptor class. Descriptor allowing adjustments to the SVG image scaling behavior. This writing system is used by most of modern languages. The content direction affects various layout structures. In LTR mode, items are typically aligned to the left. This mode also influences the direction of items in certain layouts. For instance, in a row element with LTR mode, the first item is positioned on the left, while the last item is on the right. This writing system is used by languages such as Hebrew, Arabic, and Persian. The content direction affects various layout structures. In RTL mode, items are typically aligned to the right. This mode also influences the direction of items in certain layouts. For instance, in a row element with RTL mode, the first item is positioned on the right, while the last item is on the left. Handler for adjusting the appearance of the text span, including attributes like color, size, and background. Handler for adjusting the appearance of the text span, including attributes like color, size, and background. Additionally, it provides formatting options, like displaying the page number in custom text format (e.g. Roman numerals). Limits the number of visible lines in a paragraph, truncating overflow text with an ellipsis or by hiding it to maintain layout consistency. Aligns text horizontally to the left side. Aligns text horizontally to the center, ensuring equal space on both left and right sides. Aligns content horizontally to the right side. Aligns the text horizontally to the start of the container. This method sets the horizontal alignment of the text to the start (left for left-to-right languages, right for right-to-left languages). Aligns the text horizontally to the end of the container. This method sets the horizontal alignment of the text to the end (right for left-to-right languages, left for right-to-left languages). Justifies the text within its container. This method sets the horizontal alignment of the text to be justified, meaning it aligns along both the left and right margins. This is achieved by adjusting the spacing between words and characters as necessary so that each line of text stretches from one end of the text column to the other. This creates a clean, block-like appearance for the text. Sets the font color. The font color determines the color applied to text characters, affecting their visual appearance. It also influences the default color of text decorations, such as underlines. Sets a solid background color for the text. This color fills the area behind the text or other elements, enhancing contrast and providing visual emphasis. Sets the font family of the text. A font family is a collection of related fonts that share a consistent design style but may vary in weight, style, or width. Examples of font families include Arial, Times New Roman, and Calibri. 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 the FontManager class. Actual font family (e.g. "Times New Roman", "Calibri", "Lato") or custom identifier used when invoking the FontManager method. Enables or disables font features defined by the OpenType standard, e.g. kernig, ligatures. Font features are always encoded as 4-character long strings. For example, the ligatures feature is encode as liga, while the kernig feature as kern. For a list of available features, refer to the FontFeatures class. To better understand what font features are, please consider example definitions. Please note that there are many more various font features. Most fonts support only a handful of them, having some of them enabled by default: Ligatures in typography are specific character combinations that are designed to improve the aesthetics and readability of certain letter pairs. For example, in some fonts, when you type certain combinations of letters like 'fi' or 'fl', they will be replaced with a single, joined glyph. Kerning in typography refers to the adjustment of space between characters in a proportional font. It's used to achieve a visually pleasing result by adjusting the spacing of specific character pairs. For example, in many fonts, the pair 'AV' is kerned so that the 'A' and 'V' are closer together than they would be by default. TextStyle.Default.EnableFontFeature(FontFeatures.StandardLigatures); TextStyle.Default.DisableFontFeature(FontFeatures.Kerning); Provide font feature name or use the FontFeatures class. Sets font size for the text. Font size measures the height of text characters, determining how large or small the text appears. It's worth noting that different fonts may render text with distinct visual sizes, even when assigned the same numerical font size. Allows text to wrap at any character, not just spaces. Adjusts the vertical spacing between lines of text, affecting readability and overall text layout. The added space is proportional to the text size. A value of 1 retains the original spacing. Learn more Sets the proportion of vertical spacing relative to the font size. A value greater than 1 increases the spacing, while a value less than 1 decreases it. The value must be greater than 0. A value of 1 sets the line height equal to the font size. This setting may result in insufficient vertical spacing around the text. A value of default indicates that the font metrics should be used to calculate the line spacing, which is the default behavior. Adjusts the horizontal spacing between characters in the text, affecting readability and overall visual style. The adjustment is proportional to the text size. Learn more Sets the proportion by which the horizontal space between characters is changed. A value of 0 maintains the original spacing. When the value is positive, the text is more spread out. When it is negative, the text is more condensed. Adjusts the horizontal spacing between words in the text, affecting readability and overall visual style. The adjustment is proportional to the text size. Sets the proportion by which the horizontal space between words is changed. A value of 0 maintains the original spacing. When the value is positive, the text is more spread out. When it is negative, the text is more condensed. Adjusts the vertical gap between successive paragraphs (separated by line breaks), helping to visually separate blocks of text for improved readability. Specifies the horizontal offset of the first line in a paragraph. Commonly used to visually separate paragraphs in a block of text. Renders text with an italic effect, where letters are slightly slanted to the right. Commonly used for emphasis or to distinguish specific words. Draws a line through the middle of the text. Draws a line under the text. Draws a line above the text. You can control the decoration visuals by setting color and style (solid, double, wavy, dotted, dashed) of the line. Applies for the following text decorations: striketrough, underline, overline. Sets the color of the text decoration. Changess thickness of decoration line proportionally to the provided argument. Value equal to 1 is the default thickness. Sets the decoration line to a solid straight line. Sets the decoration line to a solid double line. Sets the decoration line to wave. Sets the decoration line to dots. Sets the decoration line to dashes. Sets the font weight to "thin" (equivalent to CSS 100). Learn more Sets the font weight to "extra light" (equivalent to CSS 200). Learn more Sets the font weight to "light" (equivalent to CSS 300). Learn more Sets the font weight to "normal" or "regular (equivalent to CSS 400). Learn more Sets the font weight to "medium" (equivalent to CSS 500). Learn more Sets the font weight to "semi bold" (equivalent to CSS 600). Learn more Sets the font weight to "bold" (equivalent to CSS 700). Learn more Sets the font weight to "extra bold" (equivalent to CSS 800). Learn more Sets the font weight to "black" (equivalent to CSS 900). Learn more Sets the font weight to "extra black" (equivalent to CSS 1000). Learn more Determines the thickness of the text characters, ranging from light to bold, to create visual hierarchy or emphasis. Not all fonts support every weight. If the specified weight isn't available, the library selects the closest available option. Resets the text position and size to default, utilizing the full available line height. Learn more Sets the text style to subscript, making it smaller and positioning it below the baseline, e.g.: H₂0. Learn more Sets the text style to subscript, making it smaller and positioning it above the baseline, e.g.: y² + x² = 1. Learn more Resets the text direction, enabling content to follow the automatically detected direction. Enforces a left-to-right text direction. Enforces a right-to-left text direction. Sets a font style using the typography pattern. Learn more This API reduces the garbage collector pressure and offers the best performance. Connects to the QuestPDF Companion application to automatically preview the document being currently implemented after every code modification, without the need to recompile the code or restart the application. Learn more For details on the hot-reload functionality, please refer to the documentation of your preferred IDE. Specifies port for communication with the QuestPDF Previewer application. Default value is 12500. The hot-reload feature and QuestPDF Companion integration are available only in the .NET 6 environment or later. Please consider updating your project. Installation and usage
Features overview
This parameter is ignored.
Components serve as modular building blocks for abstracting document layouts. They promote code reusability across multiple sections or types of documents. Using a component, you can generate content based on its internal state. Learn more Consider the scenario of a company-wide page header. Instead of replicating the same header design across various documents, a single component can be created and referenced wherever needed. Syntax Description 1, 2, 3 Plain numbers indicate pages numbered from the start r1, r2 Numbers with 'r' prefix count from the end (r1 = last page) z Represents the last page (equivalent to r1) 1-5 Dash-separated ranges are inclusive 5-1 Reversed ranges list pages in descending order x1-3 Excludes specified pages from previous range :odd Selects odd-positioned pages from the resulting page-set :even Selects even-positioned pages from the resulting page-set Expression Result 1,6,4 pages 1, 6, and 4 in that order 3-7 pages 3 through 7 inclusive 7-3 pages 7, 6, 5, 4, and 3 in that order 1-z all pages in order z-1 all pages in reverse order r3-r1 the last three pages of the document r1-r3 the last three pages of the document in reverse order 1-20:even even pages from 2 to 20 5,7-9,12 pages 5, 7, 8, 9, and 12 5,7-9,12:odd pages 5, 8, and 12 (pages in odd positions from the original set of 5, 7, 8, 9, 12) 5,7-9,12:even pages 7 and 9 (pages in even positions from the original set of 5, 7, 8, 9, 12) 1-10,x3-4 pages 1 through 10 except pages 3 and 4 (1, 2, and 5 through 10) 4-10,x7-9,12-8,xr5 In a 15-page file: pages 4, 5, 6, 10, 12, 10, 9, and 8 in that order (pages 4 through 10 except 7 through 9, followed by 12 through 8 descending, except 11 which is the fifth page from the end) Specifies whether the user is permitted to add signatures and annotations to the document. Specifies whether the user is allowed to copy text and graphics from the document. Specifies whether the user is permitted to insert, rotate, or delete pages within the document. Specifies whether the user can print the document. Specifies whether the user is permitted to insert, rotate, or delete pages within the document. Specifies whether the user is allowed to fill out existing form fields in the document. Determines whether the document's metadata is included in encryption.
================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/emsdk.txt ================================================ Copyright (c) 2018 Emscripten authors (see AUTHORS in Emscripten) 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ---------------------------------------------------------------------------- This is the MIT/Expat Licence. For more information see: 1. http://www.opensource.org/licenses/mit-license.php 2. http://en.wikipedia.org/wiki/MIT_License ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/expat.txt ================================================ Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper Copyright (c) 2001-2022 Expat maintainers 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/harfbuzz.txt ================================================ HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. For parts of HarfBuzz that are licensed under different licenses see individual files names COPYING in subdirectories where applicable. Copyright © 2010-2022 Google, Inc. Copyright © 2015-2020 Ebrahim Byagowi Copyright © 2019,2020 Facebook, Inc. Copyright © 2012,2015 Mozilla Foundation Copyright © 2011 Codethink Limited Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) Copyright © 2009 Keith Stribley Copyright © 2011 Martin Hosken and SIL International Copyright © 2007 Chris Wilson Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. Copyright © 1998-2005 David Turner and Werner Lemberg Copyright © 2016 Igalia S.L. Copyright © 2022 Matthias Clasen Copyright © 2018,2021 Khaled Hosny Copyright © 2018,2019,2020 Adobe, Inc Copyright © 2013-2015 Alexei Podtelezhnikov For full copyright notices consult the individual files in the package. Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/libgrapheme.txt ================================================ ISC-License Copyright 2019-2025 Laslo Hunhold Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/libjpeg-turbo.txt ================================================ libjpeg-turbo Licenses ====================== libjpeg-turbo is covered by two compatible BSD-style open source licenses: - The IJG (Independent JPEG Group) License, which is listed in [README.ijg](README.ijg) This license applies to the libjpeg API library and associated programs, including any code inherited from libjpeg and any modifications to that code. Note that the libjpeg-turbo SIMD source code bears the [zlib License](https://opensource.org/licenses/Zlib), but in the context of the overall libjpeg API library, the terms of the zlib License are subsumed by the terms of the IJG License. - The Modified (3-clause) BSD License, which is listed below This license applies to the TurboJPEG API library and associated programs, as well as the build system. Note that the TurboJPEG API library wraps the libjpeg API library, so in the context of the overall TurboJPEG API library, both the terms of the IJG License and the terms of the Modified (3-clause) BSD License apply. -------------------------------------------------------------------------------- libjpeg-turbo note: This file has been modified by The libjpeg-turbo Project to include only information relevant to libjpeg-turbo, to wordsmith certain sections, and to remove impolitic language that existed in the libjpeg v8 README. It is included only for reference. Please see README.md for information specific to libjpeg-turbo. The Independent JPEG Group's JPEG software ========================================== This distribution contains a release of the Independent JPEG Group's free JPEG software. You are welcome to redistribute this software and to use it for any purpose, subject to the conditions under LEGAL ISSUES, below. This software is the work of Tom Lane, Guido Vollbeding, Philip Gladstone, Bill Allombert, Jim Boucher, Lee Crocker, Bob Friesenhahn, Ben Jackson, Julian Minguillon, Luis Ortiz, George Phillips, Davide Rossi, Ge' Weijers, and other members of the Independent JPEG Group. IJG is not affiliated with the ISO/IEC JTC1/SC29/WG1 standards committee (also known as JPEG, together with ITU-T SG16). DOCUMENTATION ROADMAP ===================== This file contains the following sections: OVERVIEW General description of JPEG and the IJG software. LEGAL ISSUES Copyright, lack of warranty, terms of distribution. REFERENCES Where to learn more about JPEG. ARCHIVE LOCATIONS Where to find newer versions of this software. FILE FORMAT WARS Software *not* to get. TO DO Plans for future IJG releases. Other documentation files in the distribution are: User documentation: usage.txt Usage instructions for cjpeg, djpeg, jpegtran, rdjpgcom, and wrjpgcom. *.1 Unix-style man pages for programs (same info as usage.txt). wizard.txt Advanced usage instructions for JPEG wizards only. change.log Version-to-version change highlights. Programmer and internal documentation: libjpeg.txt How to use the JPEG library in your own programs. example.c Sample code for calling the JPEG library. structure.txt Overview of the JPEG library's internal structure. coderules.txt Coding style rules --- please read if you contribute code. Please read at least usage.txt. Some information can also be found in the JPEG FAQ (Frequently Asked Questions) article. See ARCHIVE LOCATIONS below to find out where to obtain the FAQ article. If you want to understand how the JPEG code works, we suggest reading one or more of the REFERENCES, then looking at the documentation files (in roughly the order listed) before diving into the code. OVERVIEW ======== This package contains C software to implement JPEG image encoding, decoding, and transcoding. JPEG (pronounced "jay-peg") is a standardized compression method for full-color and grayscale images. JPEG's strong suit is compressing photographic images or other types of images that have smooth color and brightness transitions between neighboring pixels. Images with sharp lines or other abrupt features may not compress well with JPEG, and a higher JPEG quality may have to be used to avoid visible compression artifacts with such images. JPEG is normally lossy, meaning that the output pixels are not necessarily identical to the input pixels. However, on photographic content and other "smooth" images, very good compression ratios can be obtained with no visible compression artifacts, and extremely high compression ratios are possible if you are willing to sacrifice image quality (by reducing the "quality" setting in the compressor.) This software implements JPEG baseline, extended-sequential, progressive, and lossless compression processes. Provision is made for supporting all variants of these processes, although some uncommon parameter settings aren't implemented yet. We have made no provision for supporting the hierarchical processes defined in the standard. We provide a set of library routines for reading and writing JPEG image files, plus two sample applications "cjpeg" and "djpeg", which use the library to perform conversion between JPEG and some other popular image file formats. The library is intended to be reused in other applications. In order to support file conversion and viewing software, we have included considerable functionality beyond the bare JPEG coding/decoding capability; for example, the color quantization modules are not strictly part of JPEG decoding, but they are essential for output to colormapped file formats or colormapped displays. These extra functions can be compiled out of the library if not required for a particular application. We have also included "jpegtran", a utility for lossless transcoding between different JPEG processes, and "rdjpgcom" and "wrjpgcom", two simple applications for inserting and extracting textual comments in JFIF files. The emphasis in designing this software has been on achieving portability and flexibility, while also making it fast enough to be useful. In particular, the software is not intended to be read as a tutorial on JPEG. (See the REFERENCES section for introductory material.) Rather, it is intended to be reliable, portable, industrial-strength code. We do not claim to have achieved that goal in every aspect of the software, but we strive for it. We welcome the use of this software as a component of commercial products. No royalty is required, but we do ask for an acknowledgement in product documentation, as described under LEGAL ISSUES. LEGAL ISSUES ============ In plain English: 1. We don't promise that this software works. (But if you find any bugs, please let us know!) 2. You can use this software for whatever you want. You don't have to pay us. 3. You may not pretend that you wrote this software. If you use it in a program, you must acknowledge somewhere in your documentation that you've used the IJG code. In legalese: The authors make NO WARRANTY or representation, either express or implied, with respect to this software, its quality, accuracy, merchantability, or fitness for a particular purpose. This software is provided "AS IS", and you, its user, assume the entire risk as to its quality and accuracy. This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. All Rights Reserved except as specified below. Permission is hereby granted to use, copy, modify, and distribute this software (or portions thereof) for any purpose, without fee, subject to these conditions: (1) If any part of the source code for this software is distributed, then this README file must be included, with this copyright and no-warranty notice unaltered; and any additions, deletions, or changes to the original files must be clearly indicated in accompanying documentation. (2) If only executable code is distributed, then the accompanying documentation must state that "this software is based in part on the work of the Independent JPEG Group". (3) Permission for use of this software is granted only if the user accepts full responsibility for any undesirable consequences; the authors accept NO LIABILITY for damages of any kind. These conditions apply to any software derived from or based on the IJG code, not just to the unmodified library. If you use our work, you ought to acknowledge us. Permission is NOT granted for the use of any IJG author's name or company name in advertising or publicity relating to this software or products derived from it. This software may be referred to only as "the Independent JPEG Group's software". We specifically permit and encourage the use of this software as the basis of commercial products, provided that all warranty or liability claims are assumed by the product vendor. REFERENCES ========== We recommend reading one or more of these references before trying to understand the innards of the JPEG software. The best short technical introduction to the JPEG compression algorithm is Wallace, Gregory K. "The JPEG Still Picture Compression Standard", Communications of the ACM, April 1991 (vol. 34 no. 4), pp. 30-44. (Adjacent articles in that issue discuss MPEG motion picture compression, applications of JPEG, and related topics.) If you don't have the CACM issue handy, a PDF file containing a revised version of Wallace's article is available at http://www.ijg.org/files/Wallace.JPEG.pdf. The file (actually a preprint for an article that appeared in IEEE Trans. Consumer Electronics) omits the sample images that appeared in CACM, but it includes corrections and some added material. Note: the Wallace article is copyright ACM and IEEE, and it may not be used for commercial purposes. A somewhat less technical, more leisurely introduction to JPEG can be found in "The Data Compression Book" by Mark Nelson and Jean-loup Gailly, published by M&T Books (New York), 2nd ed. 1996, ISBN 1-55851-434-1. This book provides good explanations and example C code for a multitude of compression methods including JPEG. It is an excellent source if you are comfortable reading C code but don't know much about data compression in general. The book's JPEG sample code is far from industrial-strength, but when you are ready to look at a full implementation, you've got one here... The best currently available description of JPEG is the textbook "JPEG Still Image Data Compression Standard" by William B. Pennebaker and Joan L. Mitchell, published by Van Nostrand Reinhold, 1993, ISBN 0-442-01272-1. Price US$59.95, 638 pp. The book includes the complete text of the ISO JPEG standards (DIS 10918-1 and draft DIS 10918-2). The original JPEG standard is divided into two parts, Part 1 being the actual specification, while Part 2 covers compliance testing methods. Part 1 is titled "Digital Compression and Coding of Continuous-tone Still Images, Part 1: Requirements and guidelines" and has document numbers ISO/IEC IS 10918-1, ITU-T T.81. Part 2 is titled "Digital Compression and Coding of Continuous-tone Still Images, Part 2: Compliance testing" and has document numbers ISO/IEC IS 10918-2, ITU-T T.83. The JPEG standard does not specify all details of an interchangeable file format. For the omitted details, we follow the "JFIF" conventions, revision 1.02. JFIF version 1 has been adopted as ISO/IEC 10918-5 (05/2013) and Recommendation ITU-T T.871 (05/2011): Information technology - Digital compression and coding of continuous-tone still images: JPEG File Interchange Format (JFIF). It is available as a free download in PDF file format from https://www.iso.org/standard/54989.html and http://www.itu.int/rec/T-REC-T.871. A PDF file of the older JFIF 1.02 specification is available at http://www.w3.org/Graphics/JPEG/jfif3.pdf. The TIFF 6.0 file format specification can be obtained from http://mirrors.ctan.org/graphics/tiff/TIFF6.ps.gz. The JPEG incorporation scheme found in the TIFF 6.0 spec of 3-June-92 has a number of serious problems. IJG does not recommend use of the TIFF 6.0 design (TIFF Compression tag 6). Instead, we recommend the JPEG design proposed by TIFF Technical Note #2 (Compression tag 7). Copies of this Note can be obtained from http://www.ijg.org/files/. It is expected that the next revision of the TIFF spec will replace the 6.0 JPEG design with the Note's design. Although IJG's own code does not support TIFF/JPEG, the free libtiff library uses our library to implement TIFF/JPEG per the Note. ARCHIVE LOCATIONS ================= The "official" archive site for this software is www.ijg.org. The most recent released version can always be found there in directory "files". The JPEG FAQ (Frequently Asked Questions) article is a source of some general information about JPEG. It is available at http://www.faqs.org/faqs/jpeg-faq. FILE FORMAT COMPATIBILITY ========================= This software implements ITU T.81 | ISO/IEC 10918 with some extensions from ITU T.871 | ISO/IEC 10918-5 (JPEG File Interchange Format-- see REFERENCES). Informally, the term "JPEG image" or "JPEG file" most often refers to JFIF or a subset thereof, but there are other formats containing the name "JPEG" that are incompatible with the original JPEG standard or with JFIF (for instance, JPEG 2000 and JPEG XR). This software therefore does not support these formats. Indeed, one of the original reasons for developing this free software was to help force convergence on a common, interoperable format standard for JPEG files. JFIF is a minimal or "low end" representation. TIFF/JPEG (TIFF revision 6.0 as modified by TIFF Technical Note #2) can be used for "high end" applications that need to record a lot of additional data about an image. TO DO ===== Please send bug reports, offers of help, etc. to jpeg-info@jpegclub.org. -------------------------------------------------------------------------------- The Modified (3-clause) BSD License =================================== Copyright (C)2009-2023 D. R. Commander. All Rights Reserved.
Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the libjpeg-turbo Project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/libpng.txt ================================================ COPYRIGHT NOTICE, DISCLAIMER, and LICENSE ========================================= PNG Reference Library License version 2 --------------------------------------- * Copyright (c) 1995-2024 The PNG Reference Library Authors. * Copyright (c) 2018-2024 Cosmin Truta. * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. * Copyright (c) 1996-1997 Andreas Dilger. * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. The software is supplied "as is", without warranty of any kind, express or implied, including, without limitation, the warranties of merchantability, fitness for a particular purpose, title, and non-infringement. In no event shall the Copyright owners, or anyone distributing the software, be liable for any damages or other liability, whether in contract, tort or otherwise, arising from, out of, or in connection with the software, or the use or other dealings in the software, even if advised of the possibility of such damage. Permission is hereby granted to use, copy, modify, and distribute this software, or portions hereof, for any purpose, without fee, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated, but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This Copyright notice may not be removed or altered from any source or altered source distribution. PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) ----------------------------------------------------------------------- libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are derived from libpng-1.0.6, and are distributed according to the same disclaimer and license as libpng-1.0.6 with the following individuals added to the list of Contributing Authors: Simon-Pierre Cadieux Eric S. Raymond Mans Rullgard Cosmin Truta Gilles Vollant James Yu Mandar Sahastrabuddhe Google Inc. Vadim Barkov and with the following additions to the disclaimer: There is no warranty against interference with your enjoyment of the library or against infringement. There is no warranty that our efforts or the library will fulfill any of your particular purposes or needs. This library is provided with all faults, and the entire risk of satisfactory quality, performance, accuracy, and effort is with the user. Some files in the "contrib" directory and some configure-generated files that are distributed with libpng have other copyright owners, and are released under other open source licenses. libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from libpng-0.96, and are distributed according to the same disclaimer and license as libpng-0.96, with the following individuals added to the list of Contributing Authors: Tom Lane Glenn Randers-Pehrson Willem van Schaik libpng versions 0.89, June 1996, through 0.96, May 1997, are Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, and are distributed according to the same disclaimer and license as libpng-0.88, with the following individuals added to the list of Contributing Authors: John Bowler Kevin Bracey Sam Bushell Magnus Holmgren Greg Roelofs Tom Tanner Some files in the "scripts" directory have other copyright owners, but are released under this license. libpng versions 0.5, May 1995, through 0.88, January 1996, are Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. For the purposes of this copyright and license, "Contributing Authors" is defined as the following set of individuals: Andreas Dilger Dave Martindale Guy Eric Schalnat Paul Schmidt Tim Wegner The PNG Reference Library is supplied "AS IS". The Contributing Authors and Group 42, Inc. disclaim all warranties, expressed or implied, including, without limitation, the warranties of merchantability and of fitness for any purpose. The Contributing Authors and Group 42, Inc. assume no liability for direct, indirect, incidental, special, exemplary, or consequential damages, which may result from the use of the PNG Reference Library, even if advised of the possibility of such damage. Permission is hereby granted to use, copy, modify, and distribute this source code, or portions hereof, for any purpose, without fee, subject to the following restrictions: 1. The origin of this source code must not be misrepresented. 2. Altered versions must be plainly marked as such and must not be misrepresented as being the original source. 3. This Copyright notice may not be removed or altered from any source or altered source distribution. The Contributing Authors and Group 42, Inc. specifically permit, without fee, and encourage the use of this source code as a component to supporting the PNG file format in commercial products. If you use this source code in a product, acknowledgment is not required but would be appreciated. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/libwebp.txt ================================================ Copyright (c) 2010, Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/ninja-build.txt ================================================ Apache License Version 2.0, January 2010 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/qpdf.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/readme.txt ================================================ If you believe that this folder does not include all of QuestPDF's third-party dependencies, please reach out to us at contact@questpdf.com. We will update the list as quickly as possible. Thank you! ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/skia.txt ================================================ Copyright (c) 2011 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/wuffs.txt ================================================ This software is distributed under the terms of both the MIT license and the Apache License (Version 2.0). MIT license Copyright 2023 The Wuffs Authors 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. Apache 2 license Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: Source/QuestPDF/Resources/ExternalDependencyLicenses/zlib.txt ================================================ Copyright notice: (C) 1995-2024 Jean-loup Gailly and Mark Adler This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Jean-loup Gailly Mark Adler jloup@gzip.org madler@alumni.caltech.edu ================================================ FILE: Source/QuestPDF/Resources/LatinWords.txt ================================================ ut sum eius quod ipse fuit nam in sunt cum illi esse at unum habent hoc de by calidum verbo sed quod aliqua est quod vos vel quod in de ut et a in nos potest ex alia erant quibus facite eorum tempore si voluntas quam Dixitque an quisque indica quod set tribus volo aer etiam etiam fabula parva finem posuit domum lego manibus portus magna eamque addunt etiam terra hic necesse magnum princeps talis sequitur actum quid quaeris homines mutatio abiit lucem quaedam off opus domus ipsum tentant nos rursus pecus punctum mater orbis prope ædificabis ipsum terra pater aliqua novum opus pars accipe adepto pro eo factum vivo ubi postquam rursus parum nisi per hominis anno factum ostende omnis bonum me dabit nostrum sub nomen ipsum per iustus forma sententia magna puto dicunt adiuva humilis line differunt vicissim causa tantum significant antequam movemur ius puer Vetus etiam idem quae omnes ibi cum ascendit usus tuus modo de multis tunc eorum scribo utinam sicut ita haec eius longis fac aliquid video eo duobus habet respice more die potuit vade venit Feceruntque numerus tuba canerent nulla maxime populus mea supra scitis aquam quam call prima qui ut descendit latere fuit nunc invenies caput stant ipse page ut patriae invenit dicendum scholae crescat studiorum etiam discant herba Cover cibum solis quatuor inter status custodi oculus numquam novissime dimitte cogitavit urbem lignum transire fundum difficile initium ut fabula Viderunt omnes tantum mare hauriret reliquit quondam currunt non cum turba proxime noctis ipsum vita pauci aquilonem liber ferte tulit scientia manducare locus amicitia coeperunt idea pisces montem subsisto quondam basi audite equo sectis certus vigilate colorem faciem lignum main aperi videtur simul postero album filii incipe obtinuit ambulate exemplum relevare cartam group semper musica eorum et caracterem saepe litteris usque mille passuum fluvio car pedes cura secundo sufficit patet puella more adulescens parata supra in perpetuum red album tametsi sentio Disputatio avem mox corpus canem familia dirige aut statum relinquo canticum metiretur ostium Vestibulum nigrum breves numerales class spiritus quaestio fieri completum navem area dimidiam petræ ut ignis austri forsit frustrum nuntiavit cognovit factum cum top omnis rex platea inch multiplicabo aliquid scilicet manete rotam pleni vi hyacintho obiectum decernere superficies abyssus lunam insulam pede ratio occupatus aliquam testimonium navicula commune aurum potest planum pro eo siccum admiramini rideat milia ago cucurrit reprehendo ludum figura aequat calidum requisierit adduxerunt calor nix piget adducet etiam distant imple orientem pingere lingua in unitas potestatem urbem Postremo quaedam fuge ceciderit ducit clamor obscuro apparatus note exspecta consilium figura stella box noun agro Reliqua recte potest libra factum pulchritudo coegi stetit continent ante docebit week ultima deditque viridem o acutus evolvere oceanus calidum absque minute confortare peculiarem animus retro patet caudae producere eo locus audivit optimum hora melius verum per centum quinque memores step mane tenent occasu terra interest attingere ieiunium verbo cantabo audite sex mensamque peregrinationes minor mane decem simplex plures vocali in bellum dormivitque contra exemplar tardus centrum amant aliquis pecuniam serviant appareant via tabula pluviae imperium regunt attrahendam frigus Observate vox navitas hunt probabile cubili frater ovum ride cell Credere forsan colligunt repentino Numerabitis quadratum ideo longitudinis repraesentant art subiectum regionem amplitudo variant habita dicere pondus generalis glaciem materia circulus par comprehendo divide syllabae sensi magnificum pila sed unda concrescunt cor sum praesens magna chorea engine locus brachium wide vela material fractionem saltus sedebitis generis fenestram store aestas erant somnum probant agitur leg exercitium murum captiones montem volunt caelum tabulam gaudium winter sat scriptum fera instrumentum custodivit vitro fœnum vitulus officium ore signum visit past molli fun splendidum Vestibulum tempestas mense million ferre peroratum beati spero flos induere mirum abiit commercia melodiam trinus officium accipiet row os exactam signum mortuus minimus angustia jubila nisi scripsit semen tone iungere suadeant mundi aspiret dominae navale surgere malum ictus olei sanguis tangerent crevit cent miscere bigas wire cost perierat brunneis induere hortum aequales misit elige cecidit apta influunt pulchrae ripam colligunt salvum imperium decimales aurem aliud prorsus fregit ita mediam occides filius stagnum momento statera magna ver observa puer rectas consonans gentem dictionary lac celeritate methodo organo redde saeculi section habitus nubes mirantique quietum lapis vegrandis ascenditur frigus consilium pauper multum experimento imo key ferrum una lignum flat viginti pelle risu rimula violet salire Praesent octo villa concurrunt radix emptum resuscitabo solve ferrum sive dis septem paragraph tertia numquid Posside capillum describere cocus pavimentum aut ex comburet tumulus salvum cat century considerate type legem aliquam litore rescriptum phrase silentium tall arenam solum volumen temperatus digitus industria value pugna mendacium percute excitant naturalis visum sensum capitale nolo cathedra periculum fructus dives densa miles aliquid operantur praxi separatum difficile medicus obsecro protegat meridies seges modern aliquid ictus discipulo anguli pars copia cuius locant orbis character insect captus tempus indica radio Locutusque atom hominis rerum effectus electrica sperare osse cogitis imaginari praestare conveniunt ita mitis mulier dux Suspicor necesse acutum alae partum proximus lava Biblia potius turba frumentum compare poem nervo tintinnabulum ex cibum fricabis tube celebre pupa fluvius timore visum tenuis triangulo planeta festinate princeps coloniam horologium mea tatem intrant major recentes quaerere mitto flavis gun sino print mortuus locum deserto sectam current vitae surrexit perveniunt magister semita parente litore divisione linteum substantia gratiam Pertinent post expendas chorda adipem gavisus original share statio Pater panem Testificor proprium bar offer segmentum servus anas instant ipsum gradum frequentare pullus carissimi hostem respondeo potum incididunt auxilium oratio natura range vapor motus semita liquidum stipes intelligantur quotus dentium testa cervicibus dolor sugar mortem bellum peritia mulieres tempore solutio magnes argentum gratias ramum par etiamne maxime fig timere ingenti soror ferrum disputant et deinceps simile dirige experientiam Octoginta pupillam emptum ductus picem tunica missa pecto cohors fune labente vincere somniatis ad vesperum condicionis pascuntur ferramentum totalis prima olfactus vallis neque duplex cathedra perseverant clausus cursus hat vende successu comitatu Auferatur eventus particularis multum natant term oppositi uxorem calceum umero expandit disponere castra confingunt bombicis natus statuere quart novem dolor sonus plana forte congregate taberna tractum mittite luceat proprietate column moleculo eligere iniuriam gray iterum requirunt lata praeparabit sal nasum plural iratus clamium continentem ================================================ FILE: Source/QuestPDF/Resources/MimeTypes.csv ================================================ 323,text/h323 3g2,video/3gpp2 3gp2,video/3gpp2 3gp,video/3gpp 3gpp,video/3gpp aac,audio/aac aaf,application/octet-stream aca,application/octet-stream accdb,application/msaccess accde,application/msaccess accdt,application/msaccess acx,application/internet-property-stream adt,audio/vnd.dlna.adts adts,audio/vnd.dlna.adts afm,application/octet-stream ai,application/postscript aif,audio/x-aiff aifc,audio/aiff aiff,audio/aiff appcache,text/cache-manifest application,application/x-ms-application art,image/x-jg asd,application/octet-stream asf,video/x-ms-asf asi,application/octet-stream asm,text/plain asr,video/x-ms-asf asx,video/x-ms-asf atom,application/atom+xml au,audio/basic avi,video/x-msvideo avif,image/avif axs,application/olescript bas,text/plain bcpio,application/x-bcpio bin,application/octet-stream bmp,image/bmp c,text/plain cab,application/vnd.ms-cab-compressed calx,application/vnd.ms-office.calx cat,application/vnd.ms-pki.seccat cdf,application/x-cdf chm,application/octet-stream class,application/x-java-applet clp,application/x-msclip cmx,image/x-cmx cnf,text/plain cod,image/cis-cod cpio,application/x-cpio cpp,text/plain crd,application/x-mscardfile crl,application/pkix-crl crt,application/x-x509-ca-cert csh,application/x-csh css,text/css csv,text/csv cur,application/octet-stream dcr,application/x-director deploy,application/octet-stream der,application/x-x509-ca-cert dib,image/bmp dir,application/x-director disco,text/xml dlm,text/dlm doc,application/msword docm,application/vnd.ms-word.document.macroEnabled.12 docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document dot,application/msword dotm,application/vnd.ms-word.template.macroEnabled.12 dotx,application/vnd.openxmlformats-officedocument.wordprocessingml.template dsp,application/octet-stream dtd,text/xml dvi,application/x-dvi dvr-ms,video/x-ms-dvr dwf,drawing/x-dwf dwp,application/octet-stream dxr,application/x-director eml,message/rfc822 emz,application/octet-stream eot,application/vnd.ms-fontobject eps,application/postscript etx,text/x-setext evy,application/envoy exe,application/vnd.microsoft.portable-executable fdf,application/vnd.fdf fif,application/fractals fla,application/octet-stream flr,x-world/x-vrml flv,video/x-flv gif,image/gif gtar,application/x-gtar gz,application/x-gzip h,text/plain hdf,application/x-hdf hdml,text/x-hdml hhc,application/x-oleobject hhk,application/octet-stream hhp,application/octet-stream hlp,application/winhlp hqx,application/mac-binhex40 hta,application/hta htc,text/x-component htm,text/html html,text/html htt,text/webviewhtml hxt,text/html ical,text/calendar icalendar,text/calendar ico,image/x-icon ics,text/calendar ief,image/ief ifb,text/calendar iii,application/x-iphone inf,application/octet-stream ins,application/x-internet-signup isp,application/x-internet-signup IVF,video/x-ivf jar,application/java-archive java,application/octet-stream jck,application/liquidmotion jcz,application/liquidmotion jfif,image/pjpeg jpb,application/octet-stream jpe,image/jpeg jpeg,image/jpeg jpg,image/jpeg js,text/javascript json,application/json jsx,text/jscript latex,application/x-latex lit,application/x-ms-reader lpk,application/octet-stream lsf,video/x-la-asf lsx,video/x-la-asf lzh,application/octet-stream m13,application/x-msmediaview m14,application/x-msmediaview m1v,video/mpeg m2ts,video/vnd.dlna.mpeg-tts m3u,audio/x-mpegurl m4a,audio/mp4 m4v,video/mp4 man,application/x-troff-man manifest,application/x-ms-manifest map,text/plain markdown,text/markdown md,text/markdown mdb,application/x-msaccess mdp,application/octet-stream me,application/x-troff-me mht,message/rfc822 mhtml,message/rfc822 mid,audio/mid midi,audio/mid mix,application/octet-stream mjs,text/javascript mmf,application/x-smaf mno,text/xml mny,application/x-msmoney mov,video/quicktime movie,video/x-sgi-movie mp2,video/mpeg mp3,audio/mpeg mp4,video/mp4 mp4v,video/mp4 mpa,video/mpeg mpe,video/mpeg mpeg,video/mpeg mpg,video/mpeg mpp,application/vnd.ms-project mpv2,video/mpeg ms,application/x-troff-ms msi,application/octet-stream mso,application/octet-stream mvb,application/x-msmediaview mvc,application/x-miva-compiled nc,application/x-netcdf nsc,video/x-ms-asf nws,message/rfc822 ocx,application/octet-stream oda,application/oda odc,text/x-ms-odc ods,application/oleobject oga,audio/ogg ogg,video/ogg ogv,video/ogg ogx,application/ogg one,application/onenote onea,application/onenote onetoc,application/onenote onetoc2,application/onenote onetmp,application/onenote onepkg,application/onenote osdx,application/opensearchdescription+xml otf,font/otf p10,application/pkcs10 p12,application/x-pkcs12 p7b,application/x-pkcs7-certificates p7c,application/pkcs7-mime p7m,application/pkcs7-mime p7r,application/x-pkcs7-certreqresp p7s,application/pkcs7-signature pbm,image/x-portable-bitmap pcx,application/octet-stream pcz,application/octet-stream pdf,application/pdf pfb,application/octet-stream pfm,application/octet-stream pfx,application/x-pkcs12 pgm,image/x-portable-graymap pko,application/vnd.ms-pki.pko pma,application/x-perfmon pmc,application/x-perfmon pml,application/x-perfmon pmr,application/x-perfmon pmw,application/x-perfmon png,image/png pnm,image/x-portable-anymap pnz,image/png pot,application/vnd.ms-powerpoint potm,application/vnd.ms-powerpoint.template.macroEnabled.12 potx,application/vnd.openxmlformats-officedocument.presentationml.template ppam,application/vnd.ms-powerpoint.addin.macroEnabled.12 ppm,image/x-portable-pixmap pps,application/vnd.ms-powerpoint ppsm,application/vnd.ms-powerpoint.slideshow.macroEnabled.12 ppsx,application/vnd.openxmlformats-officedocument.presentationml.slideshow ppt,application/vnd.ms-powerpoint pptm,application/vnd.ms-powerpoint.presentation.macroEnabled.12 pptx,application/vnd.openxmlformats-officedocument.presentationml.presentation prf,application/pics-rules prm,application/octet-stream prx,application/octet-stream ps,application/postscript psd,application/octet-stream psm,application/octet-stream psp,application/octet-stream pub,application/x-mspublisher qt,video/quicktime qtl,application/x-quicktimeplayer qxd,application/octet-stream ra,audio/x-pn-realaudio ram,audio/x-pn-realaudio rar,application/octet-stream ras,image/x-cmu-raster rf,image/vnd.rn-realflash rgb,image/x-rgb rm,application/vnd.rn-realmedia rmi,audio/mid roff,application/x-troff rpm,audio/x-pn-realaudio-plugin rtf,application/rtf rtx,text/richtext scd,application/x-msschedule sct,text/scriptlet sea,application/octet-stream setpay,application/set-payment-initiation setreg,application/set-registration-initiation sgml,text/sgml sh,application/x-sh shar,application/x-shar sit,application/x-stuffit sldm,application/vnd.ms-powerpoint.slide.macroEnabled.12 sldx,application/vnd.openxmlformats-officedocument.presentationml.slide smd,audio/x-smd smi,application/octet-stream smx,audio/x-smd smz,audio/x-smd snd,audio/basic snp,application/octet-stream spc,application/x-pkcs7-certificates spl,application/futuresplash spx,audio/ogg src,application/x-wais-source ssm,application/streamingmedia sst,application/vnd.ms-pki.certstore stl,application/vnd.ms-pki.stl sv4cpio,application/x-sv4cpio sv4crc,application/x-sv4crc svg,image/svg+xml svgz,image/svg+xml swf,application/x-shockwave-flash t,application/x-troff tar,application/x-tar tcl,application/x-tcl tex,application/x-tex texi,application/x-texinfo texinfo,application/x-texinfo tgz,application/x-compressed thmx,application/vnd.ms-officetheme thn,application/octet-stream tif,image/tiff tiff,image/tiff toc,application/octet-stream tr,application/x-troff trm,application/x-msterminal ts,video/vnd.dlna.mpeg-tts tsv,text/tab-separated-values ttc,application/x-font-ttf ttf,application/x-font-ttf tts,video/vnd.dlna.mpeg-tts txt,text/plain u32,application/octet-stream uls,text/iuls ustar,application/x-ustar vbs,text/vbscript vcf,text/x-vcard vcs,text/plain vdx,application/vnd.ms-visio.viewer vml,text/xml vsd,application/vnd.visio vss,application/vnd.visio vst,application/vnd.visio vsto,application/x-ms-vsto vsw,application/vnd.visio vsx,application/vnd.visio vtx,application/vnd.visio wasm,application/wasm wav,audio/wav wax,audio/x-ms-wax wbmp,image/vnd.wap.wbmp wcm,application/vnd.ms-works wdb,application/vnd.ms-works webm,video/webm webmanifest,application/manifest+json webp,image/webp wks,application/vnd.ms-works wm,video/x-ms-wm wma,audio/x-ms-wma wmd,application/x-ms-wmd wmf,application/x-msmetafile wml,text/vnd.wap.wml wmlc,application/vnd.wap.wmlc wmls,text/vnd.wap.wmlscript wmlsc,application/vnd.wap.wmlscriptc wmp,video/x-ms-wmp wmv,video/x-ms-wmv wmx,video/x-ms-wmx wmz,application/x-ms-wmz woff,application/font-woff woff2,font/woff2 wps,application/vnd.ms-works wri,application/x-mswrite wrl,x-world/x-vrml wrz,x-world/x-vrml wsdl,text/xml wtv,video/x-ms-wtv wvx,video/x-ms-wvx x,application/directx xaf,x-world/x-vrml xaml,application/xaml+xml xap,application/x-silverlight-app xbap,application/x-ms-xbap xbm,image/x-xbitmap xdr,text/plain xht,application/xhtml+xml xhtml,application/xhtml+xml xla,application/vnd.ms-excel xlam,application/vnd.ms-excel.addin.macroEnabled.12 xlc,application/vnd.ms-excel xlm,application/vnd.ms-excel xls,application/vnd.ms-excel xlsb,application/vnd.ms-excel.sheet.binary.macroEnabled.12 xlsm,application/vnd.ms-excel.sheet.macroEnabled.12 xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlt,application/vnd.ms-excel xltm,application/vnd.ms-excel.template.macroEnabled.12 xltx,application/vnd.openxmlformats-officedocument.spreadsheetml.template xlw,application/vnd.ms-excel xml,text/xml xof,x-world/x-vrml xpm,image/x-xpixmap xps,application/vnd.ms-xpsdocument xsd,text/xml xsf,text/xml xsl,text/xml xslt,text/xml xsn,application/octet-stream xtp,application/octet-stream xwd,image/x-xwindowdump z,application/x-compress zip,application/x-zip-compressed ================================================ FILE: Source/QuestPDF/Resources/PackageLicense.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: Source/QuestPDF/Resources/ReleaseNotes.txt ================================================ - Fixed a bug where using Page.MinSize() or Page.MaxSize() could produce unexpected results - Added more helpful validation messages for minimum and maximum size constraints in the Constrained element - Improved the welcome message with a clearer explanation of the library license ================================================ FILE: Source/QuestPDF/Settings.cs ================================================ using System; using System.Collections.Generic; using QuestPDF.Infrastructure; using QuestPDF.Skia; namespace QuestPDF { public static class Settings { /// /// Please kindly select license type that applies to your usage of the QuestPDF library. /// For more details, please check the QuestPDF License and Pricing webpage /// public static LicenseType? License { get; set; } [Obsolete("This setting is ignored since the 2023.10 version. The new infinite layout detection algorithm works automatically. You can safely remove this setting from your codebase.")] public static int DocumentLayoutExceptionThreshold { get; set; } = 250; /// /// This flag generates additional document elements to cache layout calculation results. /// In the vast majority of cases, this significantly improves performance, while slightly increasing memory consumption. /// /// Enabled by default. public static bool EnableCaching { get; set; } = true; /// /// This flag generates additional document elements to improve layout debugging experience. /// When the provided content contains size constraints impossible to meet, the library generates an enhanced exception message with additional location and layout measurement details. /// /// By default, this flag is enabled only when the debugger IS attached. public static bool EnableDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached; /// /// This flag enables checking the font glyph availability. /// If your text contains glyphs that are not present in the specified font, /// 1) when this flag is enabled: the DocumentDrawingException is thrown. OR /// 2) when this flag is disabled: placeholder characters are visible in the produced PDF file. /// Enabling this flag may slightly decrease document generation performance. /// However, it provides hints that used fonts are not sufficient to produce correct results. /// /// By default, this flag is enabled only when the debugger IS attached. public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached; /// /// Decides whether the application should use the fonts available in the environment. /// /// /// When set to true, the application will use the fonts installed on the system where it is running. This is the default behavior. /// When set to false, the application will only use the fonts that have been registered using the FontManager class in the QuestPDF library. /// This property is useful when you want to control the fonts used by your application, especially in cases where the environment might not have the necessary fonts installed. /// public static bool UseEnvironmentFonts { get; set; } = true; /// /// Specifies the collection of paths where the library will automatically search for font files to register. /// /// /// By default, this collection contains the application files path. /// You can add additional paths to this collection to include more directories for automatic font registration. /// public static ICollection FontDiscoveryPaths { get; } = new List() { Helpers.Helpers.ApplicationFilesPath }; /// /// Gets or sets the file path used for temporary storage during the document generation process. /// This path is used by various operations that require temporary files. /// public static string? TemporaryStoragePath { get; set; } static Settings() { SkNativeDependencyCompatibilityChecker.Test(); } } } ================================================ FILE: Source/QuestPDF/Skia/SkBitmap.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkBitmap : IDisposable { public IntPtr Instance { get; private set; } public SkBitmap(int width, int height) { Instance = API.bitmap_create(width, height); SkiaAPI.EnsureNotNull(Instance); } public SkData EncodeAsJpeg(int quality) { var dataInstance = API.bitmap_encode_as_jpg(Instance, quality); return new SkData(dataInstance); } public SkData EncodeAsPng() { var dataInstance = API.bitmap_encode_as_png(Instance); return new SkData(dataInstance); } public SkData EncodeAsWebp(int quality) { var dataInstance = API.bitmap_encode_as_webp(Instance, quality); return new SkData(dataInstance); } ~SkBitmap() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.bitmap_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr bitmap_create(int width, int height); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void bitmap_delete(IntPtr image); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr bitmap_encode_as_jpg(IntPtr image, int quality); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr bitmap_encode_as_png(IntPtr image); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr bitmap_encode_as_webp(IntPtr image, int quality); } } ================================================ FILE: Source/QuestPDF/Skia/SkBoxShadow.cs ================================================ using System.Runtime.InteropServices; namespace QuestPDF.Skia; [StructLayout(LayoutKind.Sequential)] internal struct SkBoxShadow { public float OffsetX; public float OffsetY; public float Blur; public uint Color; } ================================================ FILE: Source/QuestPDF/Skia/SkCanvas.cs ================================================ using System; using System.Runtime.InteropServices; using QuestPDF.Skia.Text; namespace QuestPDF.Skia; internal sealed class SkCanvas : IDisposable { public IntPtr Instance { get; private set; } private bool DisposeNativeObject { get; } public SkCanvas(IntPtr instance, bool disposeNativeObject = true) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); DisposeNativeObject = disposeNativeObject; } public static SkCanvas CreateFromBitmap(SkBitmap bitmap) { var instance = API.canvas_create_from_bitmap(bitmap.Instance); return new SkCanvas(instance); } public void Save() { API.canvas_save(Instance); } public void Restore() { API.canvas_restore(Instance); } public void Translate(float x, float y) { API.canvas_translate(Instance, x, y); } public void Scale(float factorX, float factorY) { API.canvas_scale(Instance, factorX, factorY); } public void Rotate(float degrees) { API.canvas_rotate(Instance, degrees); } public void DrawLine(SkPoint start, SkPoint end, SkPaint paint) { API.canvas_draw_line(Instance, start, end, paint.Instance); } public void DrawRectangle(SkRect position, SkPaint paint) { API.canvas_draw_rectangle(Instance, position, paint.Instance); } public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint) { API.canvas_draw_complex_border(Instance, innerRect, outerRect, paint.Instance); } public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow) { API.canvas_draw_shadow(Instance, shadowRect, shadow); } public void DrawImage(SkImage image, float width, float height) { API.canvas_draw_image(Instance, image.Instance, width, height); } public void DrawPicture(SkPicture picture) { API.canvas_draw_picture(Instance, picture.Instance); } public void DrawParagraph(SkParagraph paragraph, int? lineFrom = null, int? lineTo = null) { API.canvas_draw_paragraph(Instance, paragraph.Instance, lineFrom ?? 0, lineTo ?? int.MaxValue); } public void DrawSvgPath(string svg, uint color) { API.canvas_draw_svg_path(Instance, svg, color); } public void DrawSvg(SkSvgImage svgImage, float width, float height) { API.canvas_draw_svg(Instance, svgImage.Instance, width, height); } /// /// draws stripe pattern (red lines at 45 deegree angle) /// public void DrawOverflowArea(SkRect position) { API.canvas_draw_overflow_area(Instance, position); } public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace) { API.canvas_clip_overflow_area(Instance, availableSpace, requiredSpace); } public void ClipRectangle(SkRect clipArea) { API.canvas_clip_rectangle(Instance, clipArea); } public void ClipRoundedRectangle(SkRoundedRect rect) { API.canvas_clip_rounded_rectangle(Instance, rect); } public void AnnotateUrl(float width, float height, string url, string? description) { API.canvas_annotate_url(Instance, width, height, url, description); } public void AnnotateDestination(string destinationName) { API.canvas_annotate_destination(Instance, destinationName); } public void AnnotateDestinationLink(float width, float height, string destinationName, string? description) { API.canvas_annotate_destination_link(Instance, width, height, destinationName, description); } public SkCanvasMatrix GetCurrentMatrix() { return API.canvas_get_matrix9(Instance); } public void SetCurrentMatrix(SkCanvasMatrix matrix) { API.canvas_set_matrix9(Instance, matrix); } public void SetSemanticNodeId(int nodeId) { API.canvas_set_semantic_node_id(Instance, nodeId); } ~SkCanvas() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; if (DisposeNativeObject) API.canvas_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr canvas_create_from_bitmap(IntPtr bitmap); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_delete(IntPtr canvas); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_save(IntPtr canvas); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_restore(IntPtr canvas); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_translate(IntPtr canvas, float x, float y); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_rotate(IntPtr canvas, float angle); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_scale(IntPtr canvas, float factorX, float factorY); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_image(IntPtr canvas, IntPtr image, float width, float height); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_picture(IntPtr canvas, IntPtr picture); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_line(IntPtr canvas, SkPoint start, SkPoint end, IntPtr paint); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_rectangle(IntPtr canvas, SkRect position, IntPtr paint); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_complex_border(IntPtr canvas, SkRoundedRect innerRect, SkRoundedRect outerRect, IntPtr paint); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_shadow(IntPtr canvas, SkRoundedRect shadowRect, SkBoxShadow shadow); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_paragraph(IntPtr canvas, IntPtr paragraph, int lineFrom, int lineTo); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_svg_path(IntPtr canvas, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string svg, uint color); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_svg(IntPtr canvas, IntPtr svg, float width, float height); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_draw_overflow_area(IntPtr canvas, SkRect position); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_clip_overflow_area(IntPtr canvas, SkRect availableSpace, SkRect requiredSpace); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_clip_rectangle(IntPtr canvas, SkRect clipArea); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_clip_rounded_rectangle(IntPtr canvas, SkRoundedRect rect); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_annotate_url( IntPtr canvas, float width, float height, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string url, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string? description); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_annotate_destination(IntPtr canvas, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_annotate_destination_link( IntPtr canvas, float width, float height, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string? description); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern SkCanvasMatrix canvas_get_matrix9(IntPtr canvas); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_set_matrix9(IntPtr canvas, SkCanvasMatrix matrix); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void canvas_set_semantic_node_id(IntPtr canvas, int nodeId); } } ================================================ FILE: Source/QuestPDF/Skia/SkCanvasMatrix.cs ================================================ using System; using System.Numerics; using System.Runtime.InteropServices; namespace QuestPDF.Skia; [StructLayout(LayoutKind.Sequential)] internal struct SkCanvasMatrix { public float ScaleX; public float SkewX; public float TranslateX; public float SkewY; public float ScaleY; public float TranslateY; public float Perspective1; public float Perspective2; public float Perspective3; public static readonly SkCanvasMatrix Identity = FromMatrix4x4(Matrix4x4.Identity); public Matrix4x4 ToMatrix4x4() { return new Matrix4x4( ScaleX, SkewY, 0, 0, SkewX, ScaleY, 0, 0, 0, 0, Perspective3, 0, TranslateX, TranslateY, 0, 1); } public static SkCanvasMatrix FromMatrix4x4(Matrix4x4 matrix) { return new SkCanvasMatrix { ScaleX = matrix.M11, SkewX = matrix.M21, TranslateX = matrix.M41, SkewY = matrix.M12, ScaleY = matrix.M22, TranslateY = matrix.M42, Perspective1 = 0, Perspective2 = 0, Perspective3 = 1 }; } } ================================================ FILE: Source/QuestPDF/Skia/SkData.cs ================================================ using System; using System.IO; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkData : IDisposable { public IntPtr Instance { get; private set; } public SkData(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); } public static SkData FromFile(string filePath) { var instance = API.data_create_from_file(filePath); if (instance == IntPtr.Zero) throw new Exception($"Cannot load a file under the provided path: {filePath}."); return new SkData(instance); } public static SkData FromStream(Stream stream) { using var memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); var binaryData = memoryStream.ToArray(); return SkData.FromBinary(binaryData); } public static unsafe SkData FromBinary(byte[] data) { fixed (byte* dataPtr = data) { var instance = API.data_create_from_binary(dataPtr, data.Length); GC.KeepAlive(data); return new SkData(instance); } } public byte[] ToBytes() { var content = API.data_get_bytes(Instance); var result = new byte[content.length]; Marshal.Copy(content.bytes, result, 0, content.length); // do not Marshal.FreeHGlobal(content.bytes) // this array is managed by SkData return result; } ~SkData() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.data_unref(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr data_create_from_file([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string path); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern unsafe IntPtr data_create_from_binary(byte* arrayPointer, int arrayLength); [StructLayout(LayoutKind.Sequential)] public struct GetBytesFromDataResult { public int length; public IntPtr bytes; } [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern GetBytesFromDataResult data_get_bytes(IntPtr data); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void data_unref(IntPtr data); } } ================================================ FILE: Source/QuestPDF/Skia/SkDateTime.cs ================================================ namespace QuestPDF.Skia; internal struct SkDateTime { public short TimeZoneMinutes; public ushort Year; public byte Month; public byte DayOfWeek; public byte Day; public byte Hour; public byte Minute; public byte Second; public SkDateTime(System.DateTimeOffset dateTime) { TimeZoneMinutes = (short)(dateTime.Offset.TotalMinutes); Year = (ushort)dateTime.Year; Month = (byte)dateTime.Month; DayOfWeek = (byte)dateTime.DayOfWeek; Day = (byte)dateTime.Day; Hour = (byte)dateTime.Hour; Minute = (byte)dateTime.Minute; Second = (byte)dateTime.Second; } } ================================================ FILE: Source/QuestPDF/Skia/SkDocument.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkDocument : IDisposable { public IntPtr Instance { get; private set; } internal SkDocument(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); } public SkCanvas BeginPage(float width, float height) { var instance = API.document_begin_page(Instance, width, height); return new SkCanvas(instance, disposeNativeObject: false); } public void EndPage() { API.document_end_page(Instance); } public void Close() { API.document_close(Instance); } ~SkDocument() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.document_unref(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr document_begin_page(IntPtr document, float width, float height); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void document_end_page(IntPtr document); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void document_close(IntPtr document); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void document_unref(IntPtr document); } } ================================================ FILE: Source/QuestPDF/Skia/SkImage.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkImage : IDisposable { public IntPtr Instance { get; private set; } public readonly int Width; public readonly int Height; public readonly int EncodedDataSize; public SkImage(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); // load image details var details = API.image_get_details(Instance); Width = details.Width; Height = details.Height; EncodedDataSize = details.EncodedDataSize; GC.AddMemoryPressure(EncodedDataSize); } public static SkImage FromData(SkData data) { var instance = API.image_create_from_data(data.Instance); if (instance == IntPtr.Zero) throw new Exception("Cannot decode the provided image."); return new SkImage(instance); } /// /// Scales image only when target size is smaller than original image size. /// When image is opaque, uses the JPEG compression algorithm, otherwise uses the PNG algorithm. /// Only the JPEG compression algorithm uses the compressionQuality parameter. /// public SkImage ResizeAndCompress(int targetWidth, int targetHeight, int compressionQuality, bool downsample) { var instance = API.image_resize_and_compress(Instance, targetWidth, targetHeight, compressionQuality, downsample); return new SkImage(instance); } public static SkImage GeneratePlaceholder(int targetWidth, int targetHeight, uint firstColor, uint secondColor) { var instance = API.image_generate_placeholder(targetWidth, targetHeight, firstColor, secondColor); return new SkImage(instance); } public SkData GetEncodedData() { var dataInstance = API.image_get_encoded_data(Instance); return new SkData(dataInstance); } ~SkImage() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.image_unref(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); GC.RemoveMemoryPressure(EncodedDataSize); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr image_create_from_data(IntPtr data); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void image_unref(IntPtr image); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr image_resize_and_compress(IntPtr image, int targetImageWidth, int targetImageHeight, int compressionQuality, bool downsample); [StructLayout(LayoutKind.Sequential)] public struct SkImageDetails { public int Width; public int Height; public int EncodedDataSize; } [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern SkImageDetails image_get_details(IntPtr image); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr image_get_encoded_data(IntPtr image); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr image_generate_placeholder(int imageWidth, int imageHeight, UInt32 firstColor, UInt32 secondColor); } } ================================================ FILE: Source/QuestPDF/Skia/SkNativeDependencyCompatibilityChecker.cs ================================================ using System; using System.Linq; using System.Runtime.InteropServices; using QuestPDF.Helpers; namespace QuestPDF.Skia; internal static class SkNativeDependencyCompatibilityChecker { private const int ExpectedNativeLibraryVersion = 13; private static NativeDependencyCompatibilityChecker Instance { get; } = new() { ExecuteNativeCode = ExecuteNativeCode, CheckNativeLibraryVersion = CheckNativeLibraryVersion }; public static void Test() { Instance.Test(); } private static bool CheckNativeLibraryVersion() { try { return API.get_questpdf_version() == ExpectedNativeLibraryVersion; } catch { return false; } } private static void ExecuteNativeCode() { var random = new Random(); var a = random.Next(); var b = random.Next(); var expected = a + b; var returned = API.check_compatibility_by_calculating_sum(a, b); if (expected != returned) throw new Exception(); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern int get_questpdf_version(); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern int check_compatibility_by_calculating_sum(int a, int b); } } ================================================ FILE: Source/QuestPDF/Skia/SkPaint.cs ================================================ using System; using System.Linq; using System.Runtime.InteropServices; using QuestPDF.Infrastructure; namespace QuestPDF.Skia; internal sealed class SkPaint : IDisposable { public IntPtr Instance { get; private set; } public SkPaint() { Instance = API.paint_create(); SkiaAPI.EnsureNotNull(Instance); } public void SetSolidColor(uint color) { API.paint_set_solid_color(Instance, color); } public void SetLinearGradient(Position start, Position end, Color[] colors) { if (colors.Length == 0) throw new ArgumentException("At least one color must be provided to create a gradient.", nameof(colors)); var startPoint = new SkPoint(start.X, start.Y); var endPoint = new SkPoint(end.X, end.Y); var colorArray = colors.Select(c => c.Hex).ToArray(); API.paint_set_linear_gradient(Instance, startPoint, endPoint, colorArray.Length, colorArray); } public void SetStroke(float thickness) { API.paint_set_stroke(Instance, thickness); } public void SetDashedPathEffect(float[] intervals) { if (intervals.Length == 0) throw new ArgumentException("At least one interval must be provided to create a dashed path effect.", nameof(intervals)); if (intervals.Length % 2 != 0) throw new ArgumentException("The intervals array must contain an even number of elements.", nameof(intervals)); API.paint_set_dashed_path_effect(Instance, intervals.Length, intervals); } ~SkPaint() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.paint_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr paint_create(); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paint_delete(IntPtr paint); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paint_set_solid_color(IntPtr paint, uint color); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paint_set_linear_gradient(IntPtr paint, SkPoint start, SkPoint end, int colorsLength, uint[] colors); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paint_set_stroke(IntPtr paint, float thickness); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paint_set_dashed_path_effect(IntPtr paint, int arrayLength, float[] intervals); } } ================================================ FILE: Source/QuestPDF/Skia/SkPdfDocument.cs ================================================ using System; using System.Runtime.InteropServices; using System.Xml; namespace QuestPDF.Skia; [StructLayout(LayoutKind.Sequential)] internal struct SkPdfDocumentMetadata { public IntPtr Title; // string public IntPtr Author; // string public IntPtr Subject; // string public IntPtr Keywords; // string public IntPtr Creator; // string public IntPtr Producer; // string public IntPtr Language; // string public SkDateTime CreationDate; public SkDateTime ModificationDate; public PDFA_Conformance PDFA_Conformance; public PDFUA_Conformance PDFUA_Conformance; [MarshalAs(UnmanagedType.I1)] public bool CompressDocument; public float RasterDPI; public IntPtr SemanticNodeRoot; } internal enum PDFA_Conformance { None = 0, PDFA_1A = 1, PDFA_1B = 2, PDFA_2A = 3, PDFA_2B = 4, PDFA_2U = 5, PDFA_3A = 6, PDFA_3B = 7, PDFA_3U = 8 } internal enum PDFUA_Conformance { None = 0, PDFUA_1 = 1 } internal static class SkPdfDocument { public static SkDocument Create(SkWriteStream stream, SkPdfDocumentMetadata metadata) { var instance = API.pdf_document_create(stream.Instance, metadata); return new SkDocument(instance); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr pdf_document_create(IntPtr stream, SkPdfDocumentMetadata metadata); } } ================================================ FILE: Source/QuestPDF/Skia/SkPdfTag.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; namespace QuestPDF.Skia; internal sealed class SkPdfTag : IDisposable { public IntPtr Instance { get; private set; } public int NodeId { get; set; } public string Type { get; set; } = ""; public string? Alt { get; set; } public string? Lang { get; set; } private ICollection? Children { get; set; } private SkPdfTag(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); } public static SkPdfTag Create(int nodeId, string? type, string? alt, string? lang) { var instance = API.pdf_structure_element_create(nodeId, type, alt, lang); return new SkPdfTag(instance) { NodeId = nodeId, Type = type ?? "", Alt = alt, Lang = lang }; } public void SetChildren(ICollection children) { Children = children; var childrenArray = children.ToArray(); var childrenPointers = childrenArray.Select(c => c.Instance).ToArray(); var unmanagedArray = Marshal.AllocHGlobal(IntPtr.Size * childrenPointers.Length); Marshal.Copy(childrenPointers, 0, unmanagedArray, childrenPointers.Length); API.pdf_structure_element_set_children(Instance, unmanagedArray, childrenPointers.Length); Marshal.FreeHGlobal(unmanagedArray); } public void AddAttribute(string owner, string name, object value) { // for some reason, other marshaling approaches do not work var ownerBytes = Encoding.ASCII.GetBytes(owner + "\0"); var nameBytes = Encoding.ASCII.GetBytes(name + "\0"); if (value is string textValue) { var valueBytes = Encoding.ASCII.GetBytes(textValue + "\0"); API.pdf_structure_element_add_attribute_text(Instance, ownerBytes, nameBytes, valueBytes); } else if (value is int intValue) { API.pdf_structure_element_add_attribute_integer(Instance, ownerBytes, nameBytes, intValue); } else if (value is float floatValue) { API.pdf_structure_element_add_attribute_float(Instance, ownerBytes, nameBytes, floatValue); } else if (value is float[] floatArray) { API.pdf_structure_element_add_attribute_float_array(Instance, ownerBytes, nameBytes, floatArray, floatArray.Length); } else if (value is int[] nodeIds) { API.pdf_structure_element_add_attribute_node_ids(Instance, ownerBytes, nameBytes, nodeIds, nodeIds.Length); } else { throw new ArgumentException($"Unsupported attribute value type: {value.GetType()}"); } } ~SkPdfTag() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; // to dispose the entire tree, it is enough to invoke the pdf_structure_element_delete method on the root element // root's children should be only marked as disposed DisposeChildren(this); API.pdf_structure_element_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); static void DisposeChildren(SkPdfTag parent) { if (parent.Children == null) return; foreach (var child in parent.Children) { child.Instance = IntPtr.Zero; GC.SuppressFinalize(child); DisposeChildren(child); } } } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr pdf_structure_element_create( int nodeId, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string type, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string alt, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string lang); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void pdf_structure_element_set_children(IntPtr element, IntPtr children, int count); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void pdf_structure_element_add_attribute_text(IntPtr element, byte[] owner, byte[] name, byte[] value); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void pdf_structure_element_add_attribute_integer(IntPtr element, byte[] owner, byte[] name, int value); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void pdf_structure_element_add_attribute_float(IntPtr element, byte[] owner, byte[] name, float value); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void pdf_structure_element_add_attribute_float_array(IntPtr element, byte[] owner, byte[] name, float[] array, int arrayLength); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void pdf_structure_element_add_attribute_node_ids(IntPtr element, byte[] owner, byte[] name, int[] array, int arrayLength); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void pdf_structure_element_delete(IntPtr element); } } ================================================ FILE: Source/QuestPDF/Skia/SkPicture.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkPicture : IDisposable { public IntPtr Instance { get; private set; } public SkPicture(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); } public static SkPicture Deserialize(SkData data) { var instance = API.picture_deserialize(data.Instance); return new SkPicture(instance); } public SkData Serialize() { var dataInstance = API.picture_serialize(Instance); return new SkData(dataInstance); } ~SkPicture() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.picture_unref(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void picture_unref(IntPtr picture); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr picture_serialize(IntPtr picture); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr picture_deserialize(IntPtr data); } } ================================================ FILE: Source/QuestPDF/Skia/SkPictureRecorder.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkPictureRecorder : IDisposable { public IntPtr Instance { get; private set; } private bool IsRecording { get; set; } public SkPictureRecorder() { Instance = API.picture_recorder_create(); SkiaAPI.EnsureNotNull(Instance); } public SkCanvas BeginRecording(float width, float height) { var canvasInstance = API.picture_recorder_begin_recording(Instance, width, height); IsRecording = true; return new SkCanvas(canvasInstance, disposeNativeObject: false); } public SkPicture EndRecording() { var pictureInstance = API.picture_recorder_end_recording(Instance); IsRecording = false; return new SkPicture(pictureInstance); } ~SkPictureRecorder() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; if (IsRecording) { try { var picture = EndRecording(); picture.Dispose(); } catch { // ignored } } API.picture_recorder_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr picture_recorder_create(); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr picture_recorder_begin_recording(IntPtr pictureRecorder, float width, float height); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr picture_recorder_end_recording(IntPtr pictureRecorder); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void picture_recorder_delete(IntPtr pictureRecorder); } } ================================================ FILE: Source/QuestPDF/Skia/SkPoint.cs ================================================ using System.Runtime.InteropServices; namespace QuestPDF.Skia; [StructLayout(LayoutKind.Sequential)] internal struct SkPoint(float x, float y) { public float X = x; public float Y = y; } ================================================ FILE: Source/QuestPDF/Skia/SkRect.cs ================================================ using System.Runtime.InteropServices; namespace QuestPDF.Skia; [StructLayout(LayoutKind.Sequential)] internal struct SkRect { public float Left; public float Top; public float Right; public float Bottom; public SkRect(float left, float top, float right, float bottom) { Left = left; Top = top; Right = right; Bottom = bottom; } public float Width => Right - Left; public float Height => Bottom - Top; } ================================================ FILE: Source/QuestPDF/Skia/SkResourceProvider.cs ================================================ using System; using System.Runtime.InteropServices; using QuestPDF.Skia.Text; namespace QuestPDF.Skia; internal sealed class SkResourceProvider { public IntPtr Instance { get; private set; } public static SkResourceProvider Local { get; } = new(SkFontManager.Local); public static SkResourceProvider Global { get; } = new(SkFontManager.Global); internal static SkResourceProvider CurrentResourceProvider => Settings.UseEnvironmentFonts ? Global : Local; private SkResourceProvider(SkFontManager fontManager) { Instance = API.resource_provider_create(Helpers.Helpers.ApplicationFilesPath, fontManager.Instance); SkiaAPI.EnsureNotNull(Instance); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr resource_provider_create(string resourcesPath, IntPtr fontManager); } } ================================================ FILE: Source/QuestPDF/Skia/SkRoundedRect.cs ================================================ using System.Runtime.InteropServices; namespace QuestPDF.Skia; [StructLayout(LayoutKind.Sequential)] internal struct SkRoundedRect { public SkRect Rect; public SkPoint TopLeftRadius; public SkPoint TopRightRadius; public SkPoint BottomRightRadius; public SkPoint BottomLeftRadius; } ================================================ FILE: Source/QuestPDF/Skia/SkSemanticNodeSpecialId.cs ================================================ namespace QuestPDF.Skia; internal class SkSemanticNodeSpecialId { public const int Nothing = 0; public const int OtherArtifact = -1; public const int PaginationArtifact = -2; public const int PaginationHeaderArtifact = -3; public const int PaginationFooterArtifact = -4; public const int PaginationWatermarkArtifact = -5; public const int LayoutArtifact = -6; public const int PageArtifact = -7; public const int BackgroundArtifact = -8; } ================================================ FILE: Source/QuestPDF/Skia/SkSize.cs ================================================ using System.Runtime.InteropServices; namespace QuestPDF.Skia; [StructLayout(LayoutKind.Sequential)] internal struct SkSize { public float Width; public float Height; public SkSize(float width, float height) { Width = width; Height = height; } } ================================================ FILE: Source/QuestPDF/Skia/SkSvgCanvas.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkSvgCanvas { public static SkCanvas CreateSvg(float width, float height, SkWriteStream writeStream) { var bounds = new SkRect(0, 0, width, height); var instance = API.svg_create_canvas(bounds, writeStream.Instance); return new SkCanvas(instance); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr svg_create_canvas(SkRect bounds, IntPtr writeStream); } } ================================================ FILE: Source/QuestPDF/Skia/SkSvgImage.cs ================================================ using System; using System.Runtime.InteropServices; using QuestPDF.Skia.Text; namespace QuestPDF.Skia; [StructLayout(LayoutKind.Sequential)] internal struct SkSvgImageSize { public float Width; public float Height; public Unit WidthUnit; public Unit HeightUnit; public enum Unit { Unknown, Number, Percentage, Pixels, Centimeters, Millimeters, Inches, Points, Picas, // 1 Pica = 12 points } } internal sealed class SkSvgImage : IDisposable { public IntPtr Instance { get; private set; } public SkSvgImageSize Size; public SkRect ViewBox; public SkSvgImage(string svgString, SkResourceProvider resourceProvider, SkFontManager fontManager) { using var data = SkData.FromBinary(System.Text.Encoding.UTF8.GetBytes(svgString)); Instance = API.svg_create(data.Instance, resourceProvider.Instance, fontManager.Instance); if (Instance == IntPtr.Zero) throw new Exception("Cannot decode the provided SVG image."); API.svg_get_size(Instance, out Size, out ViewBox); } internal float AspectRatio { get { if (Size.WidthUnit is SkSvgImageSize.Unit.Percentage || Size.HeightUnit is SkSvgImageSize.Unit.Percentage) return ViewBox.Width / ViewBox.Height; return Size.Width / Size.Height; } } ~SkSvgImage() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.svg_unref(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr svg_create(IntPtr data, IntPtr resourceProvider, IntPtr fontManager); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void svg_unref(IntPtr svg); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void svg_get_size(IntPtr svg, out SkSvgImageSize size, out SkRect viewBox); } } ================================================ FILE: Source/QuestPDF/Skia/SkText.cs ================================================ using System; using System.Text; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkText : IDisposable { public IntPtr Instance { get; private set; } public SkText(string? text) { Instance = MarshalFromManagedToNative(text); } ~SkText() { this.WarnThatFinalizerIsReached(); Dispose(); } public static unsafe IntPtr MarshalFromManagedToNative(string? text) { if (text == null) return IntPtr.Zero; var length = Encoding.UTF8.GetByteCount(text); var nativeArray = Marshal.AllocHGlobal(length + 1); fixed (char* pText = text) { var ptr = (byte*)nativeArray; Encoding.UTF8.GetBytes(pText, text.Length, ptr, length); } Marshal.WriteByte(nativeArray, length, 0); // null termination return nativeArray; } public static implicit operator IntPtr(SkText text) => text.Instance; public void Dispose() { if (Instance != IntPtr.Zero) { Marshal.FreeHGlobal(Instance); Instance = IntPtr.Zero; } GC.SuppressFinalize(this); } } ================================================ FILE: Source/QuestPDF/Skia/SkWriteStream.cs ================================================ using System; using System.IO; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class SkWriteStream : IDisposable { public IntPtr Instance { get; private set; } private GCHandle CallbackHandle { get; } public SkWriteStream(Stream stream) { var nativeCallback = new API.ByteArrayCallback((data, size) => { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER unsafe { var span = new ReadOnlySpan((void*)data, size); stream.Write(span); } #else var managedArray = new byte[size]; Marshal.Copy(data, managedArray, 0, size); stream?.Write(managedArray, 0, managedArray.Length); #endif }); CallbackHandle = GCHandle.Alloc(nativeCallback); Instance = API.write_stream_create(nativeCallback); SkiaAPI.EnsureNotNull(Instance); } public void Flush() { API.write_stream_flush(Instance); } ~SkWriteStream() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; CallbackHandle.Free(); API.write_stream_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void ByteArrayCallback(IntPtr data, int size); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr write_stream_create(ByteArrayCallback callback); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void write_stream_flush(IntPtr stream); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void write_stream_delete(IntPtr stream); } } ================================================ FILE: Source/QuestPDF/Skia/SkXpsDocument.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal static class SkXpsDocument { public static SkDocument Create(SkWriteStream stream, float dpi) { var instance = API.xps_document_create(stream.Instance, dpi); return new SkDocument(instance); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr xps_document_create(IntPtr stream, float dpi); } } ================================================ FILE: Source/QuestPDF/Skia/SkiaAPI.cs ================================================ using System; using System.Diagnostics; using QuestPDF.Drawing.Exceptions; namespace QuestPDF.Skia; internal static class SkiaAPI { public const string LibraryName = "QuestPdfSkia"; public static void EnsureNotNull(IntPtr instance) { if (instance == IntPtr.Zero) throw new InitializationException($"QuestPDF cannot instantiate native object."); } public static void WarnThatFinalizerIsReached(this T disposableObject) where T : IDisposable { Debug.Fail($"An object of type '{typeof(T).Name}' was not disposed explicitly, and was finalized instead."); } } ================================================ FILE: Source/QuestPDF/Skia/Text/SkFontCollection.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia.Text; internal sealed class SkFontCollection : IDisposable { public IntPtr Instance { get; private set; } public SkFontCollection(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); } public static SkFontCollection Create(SkTypefaceProvider typefaceProvider, SkFontManager fontManager) { var instance = API.font_collection_create(fontManager.Instance, typefaceProvider.Instance); return new SkFontCollection(instance); } ~SkFontCollection() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.font_collection_unref(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr font_collection_create(IntPtr fontManager, IntPtr typefaceProvider); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void font_collection_unref(IntPtr fontCollection); } } ================================================ FILE: Source/QuestPDF/Skia/Text/SkFontManager.cs ================================================ using System; using System.Linq; using System.Runtime.InteropServices; namespace QuestPDF.Skia.Text; internal sealed class SkFontManager { public IntPtr Instance { get; } public static SkFontManager Local { get; } = new(API.font_manager_create_local(Settings.FontDiscoveryPaths.FirstOrDefault() ?? Helpers.Helpers.ApplicationFilesPath)); public static SkFontManager Global { get; } = new(API.font_manager_create_global()); private SkFontManager(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); } public SkTypeface CreateTypeface(SkData data) { var instance = API.font_manager_create_typeface(Instance, data.Instance); if (instance == IntPtr.Zero) throw new Exception("Cannot decode the provided font file."); return new SkTypeface(instance); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr font_manager_create_local(string path); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr font_manager_create_global(); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr font_manager_create_typeface(IntPtr fontManager, IntPtr fontData); } } ================================================ FILE: Source/QuestPDF/Skia/Text/SkParagraph.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia.Text; internal sealed class SkParagraph : IDisposable { public IntPtr Instance { get; private set; } public SkParagraph(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); } public void PlanLayout(float availableWidth) { API.paragraph_plan_layout(Instance, availableWidth); } public SkSize[] GetLineMetrics() { API.paragraph_get_line_metrics(Instance, out var array, out var arrayLength); var managedArray = new SkSize[arrayLength]; var size = Marshal.SizeOf(); for (var i = 0; i < arrayLength; i++) { var ptr = IntPtr.Add(array, i * size); managedArray[i] = Marshal.PtrToStructure(ptr); } API.paragraph_delete_line_metrics(array); return managedArray; } public int[] GetUnresolvedCodepoints() { API.paragraph_get_unresolved_codepoints(Instance, out var array, out var arrayLength); var managedArray = new int[arrayLength]; Marshal.Copy(array, managedArray, 0, arrayLength); API.paragraph_delete_unresolved_codepoints(array); return managedArray; } public SkRect[] GetPlaceholderPositions() { API.paragraph_get_placeholder_positions(Instance, out var array, out var arrayLength); var managedArray = new SkRect[arrayLength]; var size = Marshal.SizeOf(); for (var i = 0; i < arrayLength; i++) { var ptr = IntPtr.Add(array, i * size); managedArray[i] = Marshal.PtrToStructure(ptr); } API.paragraph_delete_positions(array); return managedArray; } public SkRect[] GetTextRangePositions(int rangeStart, int rangeEnd) { API.paragraph_get_text_range_positions(Instance, rangeStart, rangeEnd, out var array, out var arrayLength); var managedArray = new SkRect[arrayLength]; var size = Marshal.SizeOf(); for (var i = 0; i < arrayLength; i++) { var ptr = IntPtr.Add(array, i * size); managedArray[i] = Marshal.PtrToStructure(ptr); } API.paragraph_delete_positions(array); return managedArray; } ~SkParagraph() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.paragraph_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_plan_layout(IntPtr paragraph, float availableWidth); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_get_line_metrics(IntPtr paragraph, out IntPtr array, out int arrayLength); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_delete_line_metrics(IntPtr array); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_get_unresolved_codepoints(IntPtr paragraph, out IntPtr array, out int arrayLength); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_delete_unresolved_codepoints(IntPtr array); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_get_placeholder_positions(IntPtr paragraph, out IntPtr array, out int arrayLength); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_get_text_range_positions(IntPtr paragraph, int rangeStart, int rangeEnd, out IntPtr array, out int arrayLength); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_delete_positions(IntPtr array); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_delete(IntPtr paragraph); } } ================================================ FILE: Source/QuestPDF/Skia/Text/SkParagraphBuilder.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia.Text; [StructLayout(LayoutKind.Sequential)] internal record struct ParagraphStyleConfiguration { public TextAlign Alignment; public TextDirection Direction; public int MaxLinesVisible; public IntPtr LineClampEllipsis; // SKText internal enum TextAlign { Left, Right, Center, Justify, Start, End } internal enum TextDirection { Rtl, Ltr } } [StructLayout(LayoutKind.Sequential)] internal struct SkPlaceholderStyle { public float Width; public float Height; public PlaceholderAlignment Alignment; public PlaceholderBaseline Baseline; public float BaselineOffset; public SkPlaceholderStyle() { Width = 0; Height = 0; Alignment = PlaceholderAlignment.AboveBaseline; Baseline = PlaceholderBaseline.Alphabetic; BaselineOffset = 0; } internal enum PlaceholderAlignment { /// Match the baseline of the placeholder with the baseline. Baseline, /// Align the bottom edge of the placeholder with the baseline such that the /// placeholder sits on top of the baseline. AboveBaseline, /// Align the top edge of the placeholder with the baseline specified in /// such that the placeholder hangs below the baseline. BelowBaseline, /// Align the top edge of the placeholder with the top edge of the font. /// When the placeholder is very tall, the extra space will hang from /// the top and extend through the bottom of the line. Top, /// Align the bottom edge of the placeholder with the top edge of the font. /// When the placeholder is very tall, the extra space will rise from /// the bottom and extend through the top of the line. Bottom, /// Align the middle of the placeholder with the middle of the text. When the /// placeholder is very tall, the extra space will grow equally from /// the top and bottom of the line. Middle, } internal enum PlaceholderBaseline { Alphabetic, Ideographic } } record ParagraphStyle { public ParagraphStyleConfiguration.TextAlign Alignment { get; init; } public ParagraphStyleConfiguration.TextDirection Direction { get; init; } public int MaxLinesVisible { get; init; } public string LineClampEllipsis { get; init; } } internal sealed class SkParagraphBuilder : IDisposable { public IntPtr Instance { get; private set; } public ParagraphStyle Style { get; private set; } private SkFontCollection FontCollection { get; set; } public static SkParagraphBuilder Create(ParagraphStyle style, SkFontCollection fontCollection) { using var clampLinesEllipsis = new SkText(style.LineClampEllipsis); var paragraphStyleConfiguration = new ParagraphStyleConfiguration { Alignment = style.Alignment, Direction = style.Direction, MaxLinesVisible = style.MaxLinesVisible, LineClampEllipsis = clampLinesEllipsis.Instance }; var instance = API.paragraph_builder_create(paragraphStyleConfiguration, SkUnicode.Global.Instance, fontCollection.Instance); SkiaAPI.EnsureNotNull(instance); return new SkParagraphBuilder { Instance = instance, Style = style, FontCollection = fontCollection }; } public void AddText(string text, SkTextStyle textStyle) { API.paragraph_builder_add_text(Instance, text, textStyle.Instance); } public void AddPlaceholder(SkPlaceholderStyle placeholderStyle) { API.paragraph_builder_add_placeholder(Instance, placeholderStyle); } public SkParagraph CreateParagraph() { var instance = API.paragraph_builder_create_paragraph(Instance); return new SkParagraph(instance); } public void Reset() { API.paragraph_builder_reset(Instance); } ~SkParagraphBuilder() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; FontCollection?.Dispose(); API.paragraph_builder_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr paragraph_builder_create(ParagraphStyleConfiguration paragraphStyleConfiguration, IntPtr unicode, IntPtr fontCollection); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_builder_add_text(IntPtr paragraphBuilder, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string text, IntPtr textStyle); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_builder_add_placeholder(IntPtr paragraphBuilder, SkPlaceholderStyle placeholderStyle); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr paragraph_builder_create_paragraph(IntPtr paragraphBuilder); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_builder_reset(IntPtr paragraphBuilder); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void paragraph_builder_delete(IntPtr paragraphBuilder); } } ================================================ FILE: Source/QuestPDF/Skia/Text/SkTextStyle.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia.Text; [StructLayout(LayoutKind.Sequential)] internal struct TextStyleConfiguration { public float FontSize; public FontWeights FontWeight; public bool IsItalic; public const int FONT_FAMILIES_LENGTH = 16; [MarshalAs(UnmanagedType.ByValArray, SizeConst = FONT_FAMILIES_LENGTH)] public IntPtr[] FontFamilies; public const int FONT_FEATURES_LENGTH = 16; [MarshalAs(UnmanagedType.ByValArray, SizeConst = FONT_FEATURES_LENGTH)] public FontFeature[] FontFeatures; public uint ForegroundColor; public uint BackgroundColor; public uint DecorationColor; public float DecorationThickness; public TextDecoration DecorationType; public TextDecorationMode DecorationMode; public TextDecorationStyle DecorationStyle; public float LineHeight; // when 0, the default font metrics are used public float LetterSpacing; public float WordSpacing; public float BaselineOffset; [StructLayout(LayoutKind.Sequential)] public struct FontFeature { [MarshalAs(UnmanagedType.LPStr)] public string Name; public int Value; } public enum FontWeights { Invisible = 0, Thin = 100, ExtraLight = 200, Light = 300, Normal = 400, Medium = 500, SemiBold = 600, Bold = 700, ExtraBold = 800, Black = 900, ExtraBlack = 1000, } [Flags] public enum TextDecoration { NoDecoration = 0x0, Underline = 0x1, Overline = 0x2, LineThrough = 0x4 } public enum TextDecorationMode { Gaps, Through } public enum TextDecorationStyle { Solid, Double, Dotted, Dashed, Wavy } } internal sealed class SkTextStyle : IDisposable { public IntPtr Instance { get; private set; } public SkTextStyle(TextStyleConfiguration textStyleConfiguration) { Instance = API.text_style_create(textStyleConfiguration); SkiaAPI.EnsureNotNull(Instance); } ~SkTextStyle() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.text_style_delete(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr text_style_create(TextStyleConfiguration textStyleConfiguration); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void text_style_delete(IntPtr textStyle); } } ================================================ FILE: Source/QuestPDF/Skia/Text/SkTypeface.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia.Text; internal sealed class SkTypeface : IDisposable { public IntPtr Instance { get; private set; } public SkTypeface(IntPtr instance) { Instance = instance; SkiaAPI.EnsureNotNull(Instance); } ~SkTypeface() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; API.typeface_unref(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void typeface_unref(IntPtr typeface); } } ================================================ FILE: Source/QuestPDF/Skia/Text/SkTypefaceProvider.cs ================================================ using System; using System.Collections.Generic; using System.Runtime.InteropServices; namespace QuestPDF.Skia.Text; internal sealed class SkTypefaceProvider : IDisposable { public IntPtr Instance { get; private set; } private List Typefaces { get; } = new(); public SkTypefaceProvider() { Instance = API.typeface_font_provider_create(); SkiaAPI.EnsureNotNull(Instance); } public void AddTypefaceFromData(SkData data, string? alias = null) { var typeface = SkFontManager.Global.CreateTypeface(data); Typefaces.Add(typeface); if (alias == null) API.typeface_font_provider_add_typeface(Instance, typeface.Instance); else API.typeface_font_provider_add_typeface_with_custom_alias(Instance, typeface.Instance, alias); } ~SkTypefaceProvider() { this.WarnThatFinalizerIsReached(); Dispose(); } public void Dispose() { if (Instance == IntPtr.Zero) return; foreach (var typeface in Typefaces) typeface.Dispose(); API.typeface_font_provider_unref(Instance); Instance = IntPtr.Zero; GC.SuppressFinalize(this); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr typeface_font_provider_create(); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void typeface_font_provider_add_typeface(IntPtr typefaceProvider, IntPtr typeface); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void typeface_font_provider_add_typeface_with_custom_alias(IntPtr typefaceProvider, IntPtr typeface, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string alias); [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern void typeface_font_provider_unref(IntPtr typefaceProvider); } } ================================================ FILE: Source/QuestPDF/Skia/Text/SkUnicode.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia.Text; internal sealed class SkUnicode { public IntPtr Instance { get; private set; } public static SkUnicode Global { get; } = new(); private SkUnicode() { Instance = API.unicode_create(); } private static class API { [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr unicode_create(); } } ================================================ FILE: Source/QuestPDF/Skia/Utf8StringMarshaller.cs ================================================ using System; using System.Runtime.InteropServices; namespace QuestPDF.Skia; internal sealed class Utf8StringMarshaller : ICustomMarshaler { private static readonly Utf8StringMarshaller Instance = new(); public static ICustomMarshaler GetInstance(string? cookie) => Instance; public void CleanUpManagedData(object managedObj) { } public void CleanUpNativeData(IntPtr pNativeData) { Marshal.FreeHGlobal(pNativeData); } public int GetNativeDataSize() { return -1; } public IntPtr MarshalManagedToNative(object managedObject) { return SkText.MarshalFromManagedToNative(managedObject as string); } public object MarshalNativeToManaged(IntPtr pNativeData) { throw new NotImplementedException(); } } ================================================ FILE: Source/QuestPDF.Companion.TestRunner/Program.cs ================================================ using QuestPDF; using QuestPDF.Companion; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using QuestPDF.ReportSample; using QuestPDF.ReportSample.Layouts; Settings.License = LicenseType.Professional; //await RunGenericException(); //await RunLayoutError(); await RunSimpleDocument(); //await RunReportDocument(); //await RunDocumentWithMultiplePages(); Task RunGenericException() { return Document .Create(container => { container.Page(page => { page.Content() .PaddingVertical(1, Unit.Centimetre) .Column(x => { x.Spacing(20); x.Item().Text(Placeholders.LoremIpsum()); x.Item().Hyperlink("questpdf.com").Image(Placeholders.Image(300, 200)); throw new Exception("New exception"); }); }); }) .ShowInCompanionAsync(); } Task RunLayoutError() { return 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.Content() .PaddingVertical(1, Unit.Centimetre) .Column(x => { x.Spacing(20); x.Item().Text(Placeholders.LoremIpsum()); foreach (var i in Enumerable.Range(0, 15)) { x.Item().Background(Colors.Grey.Lighten3).MaxWidth(200).Container().Width(100 + i * 10).Height(50).Text($"Item {i}"); } }); }); }) .ShowInCompanionAsync(); } Task RunSimpleDocument() { return 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().Hyperlink("questpdf.com").Image(Placeholders.Image(200, 100)); }); page.Footer() .AlignCenter() .Text(x => { x.Span("Page "); x.CurrentPageNumber(); }); }); }) .ShowInCompanionAsync(); } Task RunReportDocument() { ImagePlaceholder.Solid = true; var model = DataSource.GetReport(); var report = new StandardReport(model); return report.ShowInCompanionAsync(); } Task RunDocumentWithMultiplePages() { return Document .Create(document => { foreach (var i in Enumerable.Range(10, 10)) { document.Page(page => { page.Size(new PageSize(i * 20, i * 30)); page.Margin(20); page.Content().Background(Placeholders.BackgroundColor()); }); } }) .ShowInCompanionAsync(); } Task RunMergedDocument() { var document1 = Document .Create(container => { container.Page(page => { page.Content() .Text("Page 1!") .SemiBold().FontSize(36).FontColor(Colors.Blue.Medium); }); }); var document2 = Document .Create(container => { container.Page(page => { page.Content() .Text("Page 2!") .SemiBold().FontSize(36).FontColor(Colors.Blue.Medium); }); }); var mergedDocument = Document.Merge(document1, document2); return mergedDocument.ShowInCompanionAsync(); } ================================================ FILE: Source/QuestPDF.Companion.TestRunner/QuestPDF.Companion.TestRunner.csproj ================================================  Exe net10.0 enable enable en ================================================ FILE: Source/QuestPDF.ConformanceTests/DecorationTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.ConformanceTests; internal class DecorationTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { QuestPDF.Settings.EnableDebugging = true; return Document .Create(document => { document.Page(page => { page.Size(600, 975); page.Margin(50); page.Content() .Decoration(decoration => { decoration.Before() .Column(column => { column.Item() .ShowOnce() .Height(50) .Width(200) .SemanticImage("First page: decoration before") .Image(Placeholders.Image); column.Item() .SkipOnce() .Text("Second page: decoration before"); }); decoration .Content() .PaddingVertical(25) .Column(column => { column.Spacing(25); foreach (var i in Enumerable.Range(1, 15)) { column.Item() .Width(200) .Height(50) .Background(Colors.Grey.Lighten3) .AlignCenter() .AlignMiddle() .Text($"Item {i}"); } }); decoration.After() .Column(column => { column.Item() .ShowOnce() .Height(50) .Width(200) .SemanticImage("First page: decoration after") .Image(Placeholders.Image); column.Item() .SkipOnce() .Text("Second page: decoration after"); }); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Figure", figure => figure.Alt("First page: decoration before")); foreach (var i in Enumerable.Range(1, 10)) root.Child("P"); root.Child("Figure", figure => figure.Alt("First page: decoration after")); foreach (var i in Enumerable.Range(1, 5)) root.Child("P"); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/DynamicTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class DynamicTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { var imageData = File.ReadAllBytes("Resources/photo.jpeg"); return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingVertical(30) .Column(column => { column.Spacing(25); column.Item().SemanticHeader1().Text("Conformance Test: Lazy"); column.Item().SemanticHeader2().Text("Before lazy"); foreach (var i in Enumerable.Range(0, 10)) column.Item().Dynamic(new DynamicComponent(i)); column.Item().SemanticHeader2().Text("After lazy"); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: Lazy")); root.Child("H2", h2 => h2.Alt("Before lazy")); foreach (var i in Enumerable.Range(0, 10)) { root.Child("Art", art => { art.Child("H3", h3 => h3.Alt($"Article {i}")); foreach (var j in Enumerable.Range(0, 10)) { art.Child("P"); } }); } root.Child("H2", h2 => h2.Alt("After lazy")); }); } internal class DynamicComponent(int index) : IDynamicComponent { public DynamicComponentComposeResult Compose(DynamicContext context) { var result = context.CreateElement(container => { container.SemanticArticle().Column(column => { column.Item().SemanticHeader3().Text($"Article {index}").Bold(); foreach (var j in Enumerable.Range(0, 10)) { column.Item().Text($"{index} - {j}"); } }); }); if (result.Size.Height > context.AvailableSize.Height) { return new DynamicComponentComposeResult { Content = context.CreateElement(container => { }), HasMoreContent = true }; } return new DynamicComponentComposeResult { Content = result, HasMoreContent = false }; } } } ================================================ FILE: Source/QuestPDF.ConformanceTests/FooterTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.ConformanceTests; internal class FooterTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingBottom(25) .Column(column => { column.Spacing(25); column.Item() .SemanticHeader1() .Text("Conformance Test: Footer") .FontSize(24) .Bold() .FontColor(Colors.Blue.Darken2); column.Item() .Text("Footer content should not be present in the semantic tree."); column.Item() .SemanticDivision() .Column(column => { foreach (var i in Enumerable.Range(1, 12)) { column.Item() .Width(200) .Height(100) .Background(Colors.Grey.Lighten2) .AlignCenter() .AlignMiddle() .Text($"Item {i}"); } }); }); page.Footer() .AlignCenter() .Text(text => { text.CurrentPageNumber(); text.Span(" / "); text.TotalPages(); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: Footer")); root.Child("P"); root.Child("Div", div => { foreach (var i in Enumerable.Range(1, 12)) div.Child("P"); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/HeaderTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.ConformanceTests; internal class HeaderTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Header() .Column(column => { column.Spacing(25); column.Item() .SemanticHeader1() .Text("Conformance Test: Header") .FontSize(24) .Bold() .FontColor(Colors.Blue.Darken2); column.Item() .ShowOnce() .Text("Only the first page of the Header should be present in the semantic tree."); column.Item() .SkipOnce() .Text("This item should NOT be present in the semantic tree."); }); page.Content() .PaddingTop(25) .SemanticDivision() .Column(column => { column.Spacing(25); foreach (var i in Enumerable.Range(1, 12)) { column.Item() .Width(200) .Height(100) .Background(Colors.Grey.Lighten2) .AlignCenter() .AlignMiddle() .Text($"Item {i}"); } }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: Header")); root.Child("P"); root.Child("Div", div => { foreach (var i in Enumerable.Range(1, 12)) div.Child("P"); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/HyperlinkInFooterTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.ConformanceTests; internal class HyperlinkInFooterTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingVertical(25) .Column(column => { column.Spacing(15); column.Item() .SemanticHeader1() .Text("Conformance Test: Hyperlink in Footer") .FontSize(24) .Bold() .FontColor(Colors.Blue.Darken2); column.Item().Text("Please find the link in the footer."); foreach (var i in Enumerable.Range(1, 25)) column.Item().Width(100).Height(50).Background(Colors.Grey.Lighten3); }); page.Footer() .SemanticLink("Link to the QuestPDF website") .Hyperlink("https://www.questpdf.com") .Text("https://www.questpdf.com") .Underline() .FontColor(Colors.Blue.Darken2); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: Hyperlink in Footer")); root.Child("P"); // the first occurrence of footer content should be treated as content, // all subsequent occurrences should be marked as artifacts. root.Child("Link", link => { link.Alt("Link to the QuestPDF website"); link.Child("P"); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/HyperlinkTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class HyperlinkTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Column(column => { column.Spacing(15); column.Item() .SemanticHeader1() .Text("Conformance Test: Hyperlinks") .FontSize(24) .Bold() .FontColor(Colors.Blue.Darken2); column.Item().Text("Please find the link below:"); column.Item() .SemanticLink("Link to the QuestPDF website") .Hyperlink("https://www.questpdf.com") .Text("QuestPDF website") .Underline() .FontColor(Colors.Blue.Darken2); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: Hyperlinks")); root.Child("P"); root.Child("Link", link => { link.Alt("Link to the QuestPDF website"); link.Child("P"); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/IgnoreTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class IgnoreTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { var photo = File.ReadAllBytes("Resources/photo.jpeg"); return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingVertical(30) .Column(column => { column.Spacing(25); column.Item().Text("This photo has semantic meaning:"); column.Item() .SemanticImage("A beautiful landscape") .Image(photo); column.Item().Text("While this one doesn't:"); column.Item() .SemanticIgnore() .Image(photo); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("P"); root.Child("Figure", figure => figure.Alt("A beautiful landscape")); root.Child("P"); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/ImageTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class ImageTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { var imageData = File.ReadAllBytes("Resources/photo.jpeg"); return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingVertical(30) .Column(column => { column.Spacing(25); column.Item() .SemanticHeader1() .Text("Conformance Test: Images") .FontSize(24) .Bold() .FontColor(Colors.Blue.Darken2); column.Item() .Width(300) .SemanticImage("Sample image description") .Column(column => { column.Item().Image(imageData); column.Item().PaddingTop(5).AlignCenter().SemanticCaption().Text("Sample image caption"); }); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: Images")); root.Child("Figure", figure => { figure.Alt("Sample image description"); figure.Child("Caption", caption => caption.Child("P")); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/LazyTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; namespace QuestPDF.ConformanceTests; internal class LazyTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { var imageData = File.ReadAllBytes("Resources/photo.jpeg"); return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingVertical(30) .Column(column => { column.Spacing(25); column.Item().SemanticHeader1().Text("Conformance Test: Lazy"); column.Item().SemanticHeader2().Text("Before lazy"); foreach (var i in Enumerable.Range(0, 10)) { column.Item() .Lazy(lazy => { lazy.SemanticArticle().Column(innerColumn => { innerColumn.Item().SemanticHeader3().Text($"Article {i}").Bold(); foreach (var j in Enumerable.Range(0, 10)) { innerColumn.Item().Text($"{i} - {j}"); } }); }); } column.Item().SemanticHeader2().Text("After lazy"); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: Lazy")); root.Child("H2", h2 => h2.Alt("Before lazy")); foreach (var i in Enumerable.Range(0, 10)) { root.Child("Art", art => { art.Child("H3", h3 => h3.Alt($"Article {i}")); foreach (var j in Enumerable.Range(0, 10)) { art.Child("P"); } }); } root.Child("H2", h2 => h2.Alt("After lazy")); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/LineTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class LineTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Column(column => { column.Spacing(25); column.Item() .SemanticHeader1() .Text("Conformance Test: Line Elements") .FontSize(24) .Bold() .FontColor(Colors.Blue.Darken2); column.Item() .Text("Line elements should be rendered but semantically treated as artifacts."); column.Item() .LineHorizontal(2) .LineColor(Colors.Red.Medium); column.Item() .Text(Placeholders.LoremIpsum()); column.Item() .LineHorizontal(4) .LineColor(Colors.Green.Medium) .LineDashPattern([6, 6, 12, 6]); column.Item() .SemanticDivision() .Background(Colors.Grey.Lighten3).Row(row => { row.RelativeItem() .PaddingVertical(25) .AlignRight() .Text("Text on the left side"); row.AutoItem() .PaddingHorizontal(25) .LineVertical(4) .LineGradient([ Colors.Blue.Lighten2, Colors.Blue.Darken2 ]); row.RelativeItem() .PaddingVertical(25) .Text("Text on the right side"); }); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: Line Elements")); root.Child("P"); root.Child("P"); root.Child("Div", div => { div.Child("P"); div.Child("P"); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/ListTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class ListTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingVertical(30) .SemanticSection() .Column(column => { column.Spacing(15); column.Item() .SemanticHeader1() .Text("Conformance Test: Lists") .FontSize(36) .Bold() .FontColor(Colors.Blue.Darken2); column.Item() .SemanticList() .Column(listColumn => { listColumn.Spacing(10); listColumn.Item() .SemanticListItem() .Row(row => { row.ConstantItem(20).SemanticListLabel().Text("1."); row.RelativeItem() .SemanticListItemBody() .Column(bodyColumn => { bodyColumn.Spacing(8); bodyColumn.Item().Text(Placeholders.Sentence()); bodyColumn.Item() .SemanticList() .Column(nestedColumn => { nestedColumn.Spacing(10); foreach (var i in Enumerable.Range(1, 4)) { nestedColumn.Item() .SemanticListItem() .Row(nestedRow => { nestedRow.ConstantItem(10) .SemanticListLabel() .Text("-"); nestedRow.RelativeItem() .SemanticListItemBody() .Text(Placeholders.Sentence()); }); } }); }); }); foreach (var i in Enumerable.Range(2, 5)) { listColumn.Item() .SemanticListItem() .Row(row => { row.ConstantItem(20) .SemanticListLabel() .Text($"{i}."); row.RelativeItem() .SemanticListItemBody() .Text(Placeholders.Sentence()); }); } }); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Sect", sect => { sect.Child("H1", h1 => h1.Alt("Conformance Test: Lists")); sect.Child("L", list => { list.Child("LI", listItem => { listItem.Child("Lbl"); listItem.Child("LBody", lBody => { lBody.Child("P"); lBody.Child("L", nestedList => { foreach (var i in Enumerable.Range(1, 4)) { nestedList.Child("LI", nestedItem => { nestedItem.Child("Lbl"); nestedItem.Child("LBody", listBody => listBody.Child("P")); }); } }); }); }); foreach (var i in Enumerable.Range(2, 5)) { list.Child("LI", listItem => { listItem.Child("Lbl"); listItem.Child("LBody", listBody => listBody.Child("P")); }); } }); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/MultiColumnTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.ConformanceTests; internal class MultiColumnTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .MultiColumn(multiColumn => { multiColumn.Spacing(75); multiColumn .Spacer() .PaddingHorizontal(25) .Background(Colors.Blue.Lighten4) .RotateLeft() .AlignMiddle() .AlignCenter() .Text("This text should not be a part of the semantic tree") .FontColor(Colors.Blue.Darken4) .Bold(); multiColumn .Content() .Column(column => { column.Spacing(25); foreach (var i in Enumerable.Range(1, 25)) { column.Item() .AlignCenter() .Background(Colors.Grey.Lighten3) .Padding(10) .Text(text => { text.Span($"Chapter {i}: ").Bold(); text.Span(Placeholders.LoremIpsum()); }); } }); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { foreach (var i in Enumerable.Range(1, 25)) root.Child("P"); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/OrderOfSemanticItemsTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; namespace QuestPDF.ConformanceTests; internal class OrderOfSemanticItemsTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { // this test checks if SemanticTag registers semantic content only in actual Skia drawing canvas return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Column(column => { column.Item() .SemanticHeader1() .Text("1 - H1"); column.Item() .SemanticHeader2() .Text("2 - H2"); column.Item() .SemanticHeader2() .Text("3 - H2"); column.Item().MultiColumn(multiColumn => { multiColumn.Spacing(75); multiColumn.Content().Column(column => { column.Item() .SemanticHeader2() .Text("4 - H2"); column.Item() .SemanticHeader3() .Text("5 - H3"); column.Item() .SemanticHeader3() .Text("6 - H3"); }); }); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("1 - H1")); root.Child("H2", h2 => h2.Alt("2 - H2")); root.Child("H2", h2 => h2.Alt("3 - H2")); root.Child("H2", h2 => h2.Alt("4 - H2")); root.Child("H3", h3 => h3.Alt("5 - H3")); root.Child("H3", h3 => h3.Alt("6 - H3")); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/QuestPDF.ConformanceTests.csproj ================================================ net10.0 enable enable en false true all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive PreserveNewest ================================================ FILE: Source/QuestPDF.ConformanceTests/Resources/zugferd-factur-x.xml ================================================ urn:cen.eu:en16931:2017 RE-20201121/508 380 20201121 1 Design (hours) Of a sample invoice 160.0000 1.0000 160.0000 1.0000 1.0000 VAT S 7.00 160.00 2 Ballons various colors, ~2000ml 0.7900 1.0000 0.7900 1.0000 400.0000 VAT S 19.00 316.00 3 Hot air „heiße Luft“ (litres) 0.0250 1.0000 0.0250 1.0000 800.0000 VAT S 19.00 20.00 AB321 Bei Spiel GmbH 12345 Ecke 12 Stadthausen DE DE136695976 2 Theodor Est 88802 Bahnstr. 42 Spielkreis DE 20201110 RE-20201121/508 EUR 42 Bank transfer DE88200800000970375700 Max Mustermann COBADEFFXXX 11.20 VAT 160.00 S 7.00 63.84 VAT 336.00 S 19.00 Zahlbar ohne Abzug bis 12.12.2020 20201212 496.00 0.00 0.00 496.00 75.04 571.04 0.00 571.04 ================================================ FILE: Source/QuestPDF.ConformanceTests/Resources/zugferd-xmp-metadata.xml ================================================ EN 16931 INVOICE factur-x.xml 1.0 ZUGFeRD PDFA Extension Schema urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# fx DocumentFileName Text external name of the embedded XML invoice file DocumentType Text external INVOICE Version Text external The actual version of the ZUGFeRD XML schema ConformanceLevel Text external The selected ZUGFeRD profile completeness ================================================ FILE: Source/QuestPDF.ConformanceTests/StyledBoxTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class StyledBoxTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingVertical(30) .SemanticSection() .Column(column => { column.Spacing(30); column.Item() .SemanticHeader1() .Text("Conformance Test: Styled Boxes") .FontSize(36) .Bold() .FontColor(Colors.Blue.Darken2); column.Item() .Background(Colors.Blue.Lighten4) .Padding(20) .Text("Background only") .FontSize(16); column.Item() .Border(2, Colors.Blue.Darken2) .Padding(20) .Text("Border only") .FontSize(16); column.Item() .Background(Colors.White) .Shadow(new BoxShadowStyle { OffsetX = 5, OffsetY = 5, Blur = 10, Spread = 5, Color = Colors.Grey.Medium }) .Padding(20) .Text("Simple shadow") .FontSize(16); column.Item() .Border(1, Colors.Purple.Lighten4) .Background(Colors.Purple.Lighten5) .CornerRadius(15) .Padding(20) .Text("Rounded corners") .FontSize(16); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Sect", sect => { sect.Child("H1", h1 => h1.Alt("Conformance Test: Styled Boxes")); foreach (var i in Enumerable.Range(1, 4)) sect.Child("P"); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/SvgTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.ConformanceTests; internal class SvgTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Column(column => { column.Spacing(25); column.Item() .SemanticHeader1() .Text("Conformance Test: SVG") .FontSize(24) .Bold() .FontColor(Colors.Blue.Darken2); column.Item() .Text("SVG content should be rendered correctly and possible to be annotated as semantic image. Image taken from: undraw.co"); column.Item() .SemanticImage("Sample SVG image description") .Svg("Resources/image.svg"); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test: SVG")); root.Child("P"); root.Child("Figure", figure => figure.Alt("Sample SVG image description")); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/Table/TableWithFooterTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.Table; internal class TableWithFooterTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Shrink() .Border(1) .BorderColor(Colors.Grey.Darken1) .SemanticTable() .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); }); foreach (var i in Enumerable.Range(1, 30)) { table.Cell().Element(CellStyle).Text($"{i}/1"); table.Cell().Element(CellStyle).Text($"{i}/2"); table.Cell().Element(CellStyle).Text($"{i}/3"); } table.Footer(footer => { footer.Cell().Element(FooterCellStyle).Text("F11"); footer.Cell().Element(FooterCellStyle).Text("F12"); footer.Cell().Element(FooterCellStyle).Text("F13"); footer.Cell().Element(FooterCellStyle).Text("F21"); footer.Cell().Element(FooterCellStyle).Text("F22"); footer.Cell().Element(FooterCellStyle).Text("F23"); }); IContainer CellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Padding(8); IContainer FooterCellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Background(Colors.Grey.Lighten3) .Padding(8); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Table", table => { table.Child("TBody", tbody => { foreach (var i in Enumerable.Range(1, 30)) { tbody.Child("TR", row => { row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); } }); table.Child("TFoot", tfoot => { tfoot.Child("TR", row => { row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); tfoot.Child("TR", row => { row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); }); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/Table/TableWithHeaderCellsSpanningMultipleColumnsTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.Table; internal class TableWithHeaderCellsSpanningMultipleColumnsTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Shrink() .Border(1) .BorderColor(Colors.Grey.Darken1) .SemanticTable() .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); }); table.Header(header => { header.Cell().RowSpan(2).Element(HeaderCellStyle).Text("Paper Type"); header.Cell().ColumnSpan(2).Element(HeaderCellStyle).Text("Width"); header.Cell().ColumnSpan(2).Element(HeaderCellStyle).Text("Height"); header.Cell().Element(HeaderCellStyle).Text("Inches"); header.Cell().Element(HeaderCellStyle).Text("Points"); header.Cell().Element(HeaderCellStyle).Text("Inches"); header.Cell().Element(HeaderCellStyle).Text("Points"); }); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("A3"); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("A4"); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("A5"); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); table.Cell().Element(CellStyle).Text(Placeholders.Decimal()); IContainer HeaderCellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Background(Colors.Grey.Lighten3) .Padding(8) .AlignMiddle() .DefaultTextStyle(x => x.Bold()); IContainer CellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Padding(8); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Table", table => { table.Child("THead", thead => { thead.Child("TR", row => { row.Child("TH", th => th .Id(5) .Attribute("Table", "RowSpan", 2) .Child("P")); row.Child("TH", th => th .Id(6) .Attribute("Table", "ColSpan", 2) .Child("P")); row.Child("TH", th => th .Id(7) .Attribute("Table", "ColSpan", 2) .Child("P")); }); thead.Child("TR", row => { row.Child("TH", th => th .Id(9) .Attribute("Table", "Headers", new[] { 6 }) .Child("P")); row.Child("TH", th => th .Id(10) .Attribute("Table", "Headers", new[] { 6 }) .Child("P")); row.Child("TH", th => th .Id(11) .Attribute("Table", "Headers", new[] { 7 }) .Child("P")); row.Child("TH", th => th .Id(12) .Attribute("Table", "Headers", new[] { 7 }) .Child("P")); }); }); table.Child("TBody", tbody => { tbody.Child("TR", row => { row.Child("TH", th => th .Id(22) .Attribute("Table", "Headers", new[] { 5 }) .Child("P")); row.Child("TD", th => th .Id(23) .Attribute("Table", "Headers", new[] { 6, 9, 22 }) .Child("P")); row.Child("TD", td => td .Id(24) .Attribute("Table", "Headers", new[] { 6, 10, 22 }) .Child("P")); row.Child("TD", td => td .Id(25) .Attribute("Table", "Headers", new[] { 7, 11, 22 }) .Child("P")); row.Child("TD", td => td .Id(26) .Attribute("Table", "Headers", new[] { 7, 12, 22 }) .Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th .Id(28) .Attribute("Table", "Headers", new[] { 5 }) .Child("P")); row.Child("TD", th => th .Id(29) .Attribute("Table", "Headers", new[] { 6, 9, 28 }) .Child("P")); row.Child("TD", td => td .Id(30) .Attribute("Table", "Headers", new[] { 6, 10, 28 }) .Child("P")); row.Child("TD", td => td .Id(31) .Attribute("Table", "Headers", new[] { 7, 11, 28 }) .Child("P")); row.Child("TD", td => td .Id(32) .Attribute("Table", "Headers", new[] { 7, 12, 28 }) .Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th .Id(34) .Attribute("Table", "Headers", new[] { 5 }) .Child("P")); row.Child("TD", th => th .Id(35) .Attribute("Table", "Headers", new[] { 6, 9, 34 }) .Child("P")); row.Child("TD", td => td .Id(36) .Attribute("Table", "Headers", new[] { 6, 10, 34 }) .Child("P")); row.Child("TD", td => td .Id(37) .Attribute("Table", "Headers", new[] { 7, 11, 34 }) .Child("P")); row.Child("TD", td => td .Id(38) .Attribute("Table", "Headers", new[] { 7, 12, 34 }) .Child("P")); }); }); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/Table/TableWithHeaderCellsSpanningMultipleRowsTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.Table; internal class TableWithHeaderCellsSpanningMultipleRowsTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Shrink() .Border(1) .BorderColor(Colors.Grey.Darken1) .SemanticTable() .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); }); table.Header(header => { header.Cell().Element(HeaderCellStyle).Text("Year"); header.Cell().Element(HeaderCellStyle).Text("Quarter"); header.Cell().Element(HeaderCellStyle).Text("Outcome"); header.Cell().Element(HeaderCellStyle).Text("Income"); }); table.Cell().RowSpan(4).AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("2024"); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q1"); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q2"); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q3"); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q4"); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().RowSpan(2).AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("2025"); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q1"); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Q2"); table.Cell().Element(CellStyle).Text(Placeholders.Price()); table.Cell().Element(CellStyle).Text(Placeholders.Price()); IContainer HeaderCellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Background(Colors.Grey.Lighten3) .Padding(8) .AlignMiddle() .DefaultTextStyle(x => x.Bold()); IContainer CellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Padding(8); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Table", table => { table.Child("THead", thead => { thead.Child("TR", row => { row.Child("TH", th => th.Id(5).Child("P")); row.Child("TH", th => th.Id(6).Child("P")); row.Child("TH", th => th.Id(7).Child("P")); row.Child("TH", th => th.Id(8).Child("P")); }); }); table.Child("TBody", tbody => { tbody.Child("TR", row => { row.Child("TH", th => th .Id(15) .Attribute("Table", "RowSpan", 4) .Attribute("Table", "Headers", new[] { 5 }) .Child("P")); row.Child("TH", th => th .Id(16) .Attribute("Table", "Headers", new[] { 6, 15 }) .Child("P")); row.Child("TD", td => td .Id(17) .Attribute("Table", "Headers", new[] { 7, 15, 16 }) .Child("P")); row.Child("TD", td => td .Id(18) .Attribute("Table", "Headers", new[] { 8, 15, 16 }) .Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th .Id(20) .Attribute("Table", "Headers", new[] { 6, 15 }) .Child("P")); row.Child("TD", td => td .Id(21) .Attribute("Table", "Headers", new[] { 7, 15, 20 }) .Child("P")); row.Child("TD", td => td .Id(22) .Attribute("Table", "Headers", new[] { 8, 15, 20 }) .Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th .Id(24) .Attribute("Table", "Headers", new[] { 6, 15 }) .Child("P")); row.Child("TD", td => td .Id(25) .Attribute("Table", "Headers", new[] { 7, 15, 24 }) .Child("P")); row.Child("TD", td => td .Id(26) .Attribute("Table", "Headers", new[] { 8, 15, 24 }) .Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th .Id(28) .Attribute("Table", "Headers", new[] { 6, 15 }) .Child("P")); row.Child("TD", td => td .Id(29) .Attribute("Table", "Headers", new[] { 7, 15, 28 }) .Child("P")); row.Child("TD", td => td .Id(30) .Attribute("Table", "Headers", new[] { 8, 15, 28 }) .Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th .Id(32) .Attribute("Table", "RowSpan", 2) .Attribute("Table", "Headers", new[] { 5 }) .Child("P")); row.Child("TH", th => th .Id(33) .Attribute("Table", "Headers", new[] { 6, 32 }) .Child("P")); row.Child("TD", td => td .Id(34) .Attribute("Table", "Headers", new[] { 7, 32, 33 }) .Child("P")); row.Child("TD", td => td .Id(35) .Attribute("Table", "Headers", new[] { 8, 32, 33 }) .Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th .Id(37) .Attribute("Table", "Headers", new[] { 6, 32 }) .Child("P")); row.Child("TD", td => td .Id(38) .Attribute("Table", "Headers", new[] { 7, 32, 37 }) .Child("P")); row.Child("TD", td => td .Id(39) .Attribute("Table", "Headers", new[] { 8, 32, 37 }) .Child("P")); }); }); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/Table/TableWithHorizontalHeadersTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.Table; internal class TableWithHorizontalHeadersTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Shrink() .Border(1) .BorderColor(Colors.Grey.Darken1) .SemanticTable() .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); }); // Row 1: Name table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Name"); table.Cell().Element(CellStyle).Text("John Smith"); table.Cell().Element(CellStyle).Text("Jane Doe"); // Row 2: Position table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Position"); table.Cell().Element(CellStyle).Text("Senior Developer"); table.Cell().Element(CellStyle).Text("UX Designer"); // Row 3: Department table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Department"); table.Cell().Element(CellStyle).Text("Engineering"); table.Cell().Element(CellStyle).Text("Design"); // Row 4: Experience table.Cell().AsSemanticHorizontalHeader().Element(HeaderCellStyle).Text("Experience"); table.Cell().Element(CellStyle).Text("5 years"); table.Cell().Element(CellStyle).Text("3 years"); IContainer HeaderCellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Background(Colors.Grey.Lighten3) .Padding(8) .AlignMiddle() .DefaultTextStyle(x => x.Bold()); IContainer CellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Padding(8); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Table", table => { table.Child("TBody", tbody => { tbody.Child("TR", row => { row.Child("TH", th => th.Attribute("Table", "Scope", "Row").Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th.Attribute("Table", "Scope", "Row").Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th.Attribute("Table", "Scope", "Row").Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); tbody.Child("TR", row => { row.Child("TH", th => th.Attribute("Table", "Scope", "Row").Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); }); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/Table/TableWithVerticalHeadersTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.Table; internal class TableWithVerticalHeadersTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Shrink() .Border(1) .BorderColor(Colors.Grey.Darken1) .SemanticTable() .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); }); table.Header(header => { header.Cell().Element(HeaderCellStyle).Text("Name"); header.Cell().Element(HeaderCellStyle).Text("Position"); header.Cell().Element(HeaderCellStyle).Text("Department"); header.Cell().Element(HeaderCellStyle).Text("Experience"); }); // Row 1: table.Cell().Element(CellStyle).Text("John Smith"); table.Cell().Element(CellStyle).Text("Senior Developer"); table.Cell().Element(CellStyle).Text("Engineering"); table.Cell().Element(CellStyle).Text("5 years"); // Row 2: table.Cell().Element(CellStyle).Text("Jane Doe"); table.Cell().Element(CellStyle).Text("UX Designer"); table.Cell().Element(CellStyle).Text("Design"); table.Cell().Element(CellStyle).Text("3 years"); IContainer HeaderCellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Background(Colors.Grey.Lighten3) .Padding(8) .AlignMiddle() .DefaultTextStyle(x => x.Bold()); IContainer CellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Padding(8); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Table", table => { table.Child("THead", thead => { thead.Child("TR", row => { row.Child("TH", th => th.Attribute("Table", "Scope", "Column").Child("P")); row.Child("TH", th => th.Attribute("Table", "Scope", "Column").Child("P")); row.Child("TH", th => th.Attribute("Table", "Scope", "Column").Child("P")); row.Child("TH", th => th.Attribute("Table", "Scope", "Column").Child("P")); }); }); table.Child("TBody", tbody => { tbody.Child("TR", row => { row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); tbody.Child("TR", row => { row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); }); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/Table/TableWithoutHeadersTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.Table; internal class TableWithoutHeadersTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Border(1) .BorderColor(Colors.Grey.Darken1) .SemanticTable() .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); }); // Row 1 table.Cell().Element(CellStyle).Text("11"); table.Cell().Element(CellStyle).Text("12"); table.Cell().Element(CellStyle).Text("13"); // Row 2 table.Cell().Element(CellStyle).Text("21"); table.Cell().Element(CellStyle).Text("22"); table.Cell().Element(CellStyle).Text("23"); // Row 3 table.Cell().Element(CellStyle).Text("31"); table.Cell().Element(CellStyle).Text("32"); table.Cell().Element(CellStyle).Text("33"); // Row 4 table.Cell().Element(CellStyle).Text("41"); table.Cell().Element(CellStyle).Text("42"); table.Cell().Element(CellStyle).Text("43"); IContainer CellStyle(IContainer container) => container .Border(1) .BorderColor(Colors.Grey.Lighten2) .Padding(8); }); }); }); } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("Table", table => { table.Child("TBody", tbody => { tbody.Child("TR", row => { row.Child("TD", th => th.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); tbody.Child("TR", row => { row.Child("TD", th => th.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); tbody.Child("TR", row => { row.Child("TD", th => th.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); tbody.Child("TR", row => { row.Child("TD", th => th.Child("P")); row.Child("TD", td => td.Child("P")); row.Child("TD", td => td.Child("P")); }); }); }); }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/TableOfContentsTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class TableOfContentsTests : ConformanceTestBase { protected override Document GetDocumentUnderTest() { return Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .PaddingVertical(30) .Column(column => { column.Item() .ExtendVertical() .AlignMiddle() .SemanticHeader1() .Text("Conformance Test:\nTable of Contents") .FontSize(36) .Bold() .FontColor(Colors.Blue.Darken2); column.Item().PageBreak(); column.Item().Element(GenerateTableOfContentsSection); column.Item().PageBreak(); column.Item().Element(GeneratePlaceholderContentSection); }); }); }); static void GenerateTableOfContentsSection(IContainer container) { container .SemanticSection() .Column(column => { column.Spacing(15); column .Item() .Text("Table of Contents") .Bold() .FontSize(20) .FontColor(Colors.Blue.Medium); column.Item() .SemanticTableOfContents() .Column(column => { column.Spacing(5); foreach (var i in Enumerable.Range(1, 10)) { column.Item() .SemanticTableOfContentsItem() .SemanticLink($"Link to section {i}") .SectionLink($"section-{i}") .Row(row => { row.ConstantItem(25).Text($"{i}."); row.AutoItem().Text(Placeholders.Label()); row.RelativeItem().PaddingHorizontal(2).TranslateY(11).LineHorizontal(1).LineDashPattern([1, 3]); row.AutoItem().Text(text => text.BeginPageNumberOfSection($"section-{i}")); }); } }); }); } static void GeneratePlaceholderContentSection(IContainer container) { container .Column(column => { foreach (var i in Enumerable.Range(1, 10)) { column.Item() .SemanticSection() .Section($"section-{i}") .Column(column => { column.Spacing(15); column.Item() .SemanticHeader2() .Text($"Section {i}") .Bold() .FontSize(20) .FontColor(Colors.Blue.Medium); column.Item().Text(Placeholders.Paragraph()); foreach (var j in Enumerable.Range(1, i)) { column.Item() .SemanticIgnore() .Width(200) .Height(150) .CornerRadius(10) .Background(Placeholders.BackgroundColor()); } }); if (i < 10) column.Item().PageBreak(); } }); } } protected override SemanticTreeNode? GetExpectedSemanticTree() { return ExpectedSemanticTree.DocumentRoot(root => { root.Child("H1", h1 => h1.Alt("Conformance Test:\nTable of Contents")); // Table of Contents Section root.Child("Sect", sect => { sect.Child("P"); sect.Child("TOC", toc => { foreach (var i in Enumerable.Range(1, 10)) { toc.Child("TOCI", toci => { toci.Child("Link", link => { link.Alt($"Link to section {i}"); link.Child("P"); // Number link.Child("P"); // Label link.Child("P"); // Page number }); }); } }); }); // Content Sections foreach (var i in Enumerable.Range(1, 10)) { root.Child("Sect", sect => { sect.Child("H2", h2 => h2.Alt($"Section {i}")); sect.Child("P"); }); } }); } } ================================================ FILE: Source/QuestPDF.ConformanceTests/TestEngine/ConformanceTestBase.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.TestEngine; [TestFixture] [Parallelizable(ParallelScope.All)] internal abstract class ConformanceTestBase { public static readonly IEnumerable PDFA_ConformanceLevels = Enum.GetValues().Skip(1); public static readonly IEnumerable PDFUA_ConformanceLevels = Enum.GetValues().Skip(1); [Test] [Explicit("Manual debugging only (override to enable)")] public void GenerateAndShow() { GetDocumentUnderTest() .WithMetadata(GetMetadata()) .WithSettings(new DocumentSettings { PDFA_Conformance = PDFA_Conformance.PDFA_3A, PDFUA_Conformance = PDFUA_Conformance.PDFUA_1 }) .GeneratePdfAndShow(); } [Test, TestCaseSource(nameof(PDFA_ConformanceLevels))] public void Test_PDFA(PDFA_Conformance conformance) { GetDocumentUnderTest() .WithMetadata(GetMetadata()) .WithSettings(new DocumentSettings { PDFA_Conformance = conformance }) .TestConformanceWithVeraPdf(); } [Test, TestCaseSource(nameof(PDFUA_ConformanceLevels))] public void Test_PDFUA(PDFUA_Conformance conformance) { GetDocumentUnderTest() .WithMetadata(GetMetadata()) .WithSettings(new DocumentSettings { PDFUA_Conformance = conformance }) .TestConformanceWithVeraPdf(); } [Test] public void TestSemanticMeaning() { var expectedSemanticTree = GetExpectedSemanticTree(); GetDocumentUnderTest() .WithSettings(new DocumentSettings { PDFUA_Conformance = PDFUA_Conformance.PDFUA_1 }) .TestSemanticTree(expectedSemanticTree); } private DocumentMetadata GetMetadata() { return new DocumentMetadata { Language = "en-US", Title = "Conformance Test", Subject = this.GetType().Name.Replace("Tests", string.Empty).PrettifyName() }; } protected abstract Document GetDocumentUnderTest(); protected abstract SemanticTreeNode? GetExpectedSemanticTree(); } ================================================ FILE: Source/QuestPDF.ConformanceTests/TestEngine/MustangConformanceTestRunner.cs ================================================ using System.Diagnostics; using System.Text; using System.Xml.Linq; using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.TestEngine; public static class MustangConformanceTestRunner { public class ValidationResult { public bool IsDocumentValid => !FailedRules.Any(); public ICollection FailedRules { get; set; } = []; public string GetErrorMessage() { var errorMessage = new StringBuilder(); foreach (var failedRule in FailedRules) { errorMessage.AppendLine($"🟥\tError"); errorMessage.AppendLine($"\t{failedRule}"); errorMessage.AppendLine(); } return errorMessage.ToString(); } } public static void TestConformance(string filePath) { var result = RunMustang(filePath); if (!result.IsDocumentValid) { Console.WriteLine(result.GetErrorMessage()); Assert.Fail(); } } private static ValidationResult RunMustang(string pdfFilePath) { if (!File.Exists(pdfFilePath)) throw new FileNotFoundException($"PDF file not found: {pdfFilePath}"); var mustangExecutablePath = Environment.GetEnvironmentVariable("MUSTANG_EXECUTABLE_PATH"); if (string.IsNullOrEmpty(mustangExecutablePath)) throw new Exception("The location path of the Mustang executable is not set. Set the MUSTANG_EXECUTABLE_PATH environment variable to the path of the Mustang executable."); var arguments = $"-jar {mustangExecutablePath} --action validate --source {pdfFilePath}"; var process = new Process { StartInfo = new ProcessStartInfo { FileName = "java", Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true } }; process.Start(); var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); return new ValidationResult() { FailedRules = XDocument .Parse(output) .Descendants("error") .Select(x => x.Value) .ToList() }; } } ================================================ FILE: Source/QuestPDF.ConformanceTests/TestEngine/SemanticAwareDrawingCanvas.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Infrastructure; using QuestPDF.Skia; using QuestPDF.Skia.Text; namespace QuestPDF.ConformanceTests.TestEngine; internal class SemanticAwareDocumentCanvas : IDocumentCanvas { internal SemanticTreeNode? SemanticTree { get; private set; } private SemanticAwareDrawingCanvas DrawingCanvas { get; } = new(); public void SetSemanticTree(SemanticTreeNode? semanticTree) { SemanticTree = semanticTree; } public void BeginDocument() { } public void EndDocument() { } public void BeginPage(Size size) { } public void EndPage() { } public IDrawingCanvas GetDrawingCanvas() { return DrawingCanvas; } } internal class SemanticAwareDrawingCanvas : IDrawingCanvas { private int CurrentSemanticNodeId { get; set; } public DocumentPageSnapshot GetSnapshot() { return new DocumentPageSnapshot(); } public void DrawSnapshot(DocumentPageSnapshot snapshot) { } public void Save() { } public void Restore() { } public void SetZIndex(int index) { } public int GetZIndex() { return 0; } public SkCanvasMatrix GetCurrentMatrix() { return SkCanvasMatrix.Identity; } public void SetMatrix(SkCanvasMatrix matrix) { } public void Translate(Position vector) { } public void Scale(float scaleX, float scaleY) { } public void Rotate(float angle) { } public void DrawLine(Position start, Position end, SkPaint paint) { if (CurrentSemanticNodeId != SkSemanticNodeSpecialId.LayoutArtifact) Assert.Fail("Detected a line drawing operation outside of layout artifact"); } public void DrawRectangle(Position vector, Size size, SkPaint paint) { if (CurrentSemanticNodeId is not (SkSemanticNodeSpecialId.BackgroundArtifact or SkSemanticNodeSpecialId.LayoutArtifact)) Assert.Fail("Detected a rectangle drawing operation outside of layout artifact"); } public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint) { if (CurrentSemanticNodeId != SkSemanticNodeSpecialId.LayoutArtifact) Assert.Fail("Detected a complex-border drawing operation outside of layout artifact"); } public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow) { if (CurrentSemanticNodeId != SkSemanticNodeSpecialId.BackgroundArtifact) Assert.Fail("Detected a shadow drawing operation outside of background artifact"); } 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 CurrentSemanticNodeId; } public void SetSemanticNodeId(int nodeId) { CurrentSemanticNodeId = nodeId; } } ================================================ FILE: Source/QuestPDF.ConformanceTests/TestEngine/SemanticTreeTestRunner.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.TestEngine; internal static class SemanticTreeTestRunner { public static void TestSemanticTree(this IDocument document, SemanticTreeNode? semanticTreeRootNode) { Settings.EnableCaching = false; Settings.EnableDebugging = false; var canvas = new SemanticAwareDocumentCanvas(); var settings = new DocumentSettings { PDFA_Conformance = PDFA_Conformance.PDFA_3A }; DocumentGenerator.RenderDocument(canvas, document, settings); CompareSemanticTrees(canvas.SemanticTree, semanticTreeRootNode); } private static void CompareSemanticTrees(SemanticTreeNode? actualRoot, SemanticTreeNode? expectedRoot) { if (expectedRoot == null && actualRoot == null) return; if (expectedRoot == null) { Assert.Fail($"Expected null but got node of type '{actualRoot?.Type}'"); return; } if (actualRoot == null) { Assert.Fail($"Expected node of type '{expectedRoot.Type}' but got null"); return; } var currentPath = new Stack(); try { Compare(actualRoot, expectedRoot); } catch { var pathText = string.Join(" -> ", currentPath.Reverse()); Console.WriteLine("Problem location"); Console.WriteLine(pathText); throw; } void Compare(SemanticTreeNode actual, SemanticTreeNode expected) { if (!currentPath.Any()) currentPath.Push(actual.Type); if (expected.NodeId != 0) Assert.That(actual.NodeId, Is.EqualTo(expected.NodeId), "NodeId mismatch"); Assert.That(actual.Type, Is.EqualTo(expected.Type), "Type mismatch"); Assert.That(actual.Alt, Is.EqualTo(expected.Alt), "Alt mismatch"); Assert.That(actual.Lang, Is.EqualTo(expected.Lang), "Lang mismatch"); CompareAttributes(); CompareChildren(); void CompareChildren() { Assert.That(actual.Children.Count, Is.EqualTo(expected.Children.Count), "Children count mismatch"); var hasMultipleChildren = actual.Children.Count > 1; foreach (var (actualChild, expectedChild) in actual.Children.Zip(expected.Children)) { var prefix = hasMultipleChildren ? $"{actual.Children.IndexOf(actualChild)}:" : ""; currentPath.Push(prefix + actualChild.Type); Compare(actualChild, expectedChild); currentPath.Pop(); } } void CompareAttributes() { Assert.That(actual.Attributes.Count, Is.EqualTo(expected.Attributes.Count), "Attribute count mismatch"); var actualList = actual.Attributes.OrderBy(a => a.Owner).ThenBy(a => a.Name); var expectedList = expected.Attributes.OrderBy(a => a.Owner).ThenBy(a => a.Name); foreach (var (actualAttribute, expectedAttribute) in actualList.Zip(expectedList)) { Assert.That(actualAttribute.Owner, Is.EqualTo(expectedAttribute.Owner), "Attribute owner mismatch"); Assert.That(actualAttribute.Name, Is.EqualTo(expectedAttribute.Name), "Attribute name mismatch"); Assert.That(actualAttribute.Value, Is.EqualTo(expectedAttribute.Value), $"Attribute value mismatch for '{expectedAttribute.Owner}:{expectedAttribute.Name}"); } } } } } internal static class ExpectedSemanticTree { public static SemanticTreeNode DocumentRoot(Action configuration) { var root = new SemanticTreeNode { Type = "Document" }; configuration(root); return root; } public static void Child(this SemanticTreeNode parent, string type, Action? configuration = null) { var child = new SemanticTreeNode { Type = type }; configuration?.Invoke(child); parent.Children.Add(child); } public static SemanticTreeNode Id(this SemanticTreeNode node, int id) { node.NodeId = id; return node; } public static SemanticTreeNode Attribute(this SemanticTreeNode node, string owner, string name, object value) { var attribute = new SemanticTreeNode.Attribute { Owner = owner, Name = name, Value = value }; node.Attributes.Add(attribute); return node; } public static SemanticTreeNode Alt(this SemanticTreeNode node, string alt) { node.Alt = alt; return node; } public static SemanticTreeNode Lang(this SemanticTreeNode node, string lang) { node.Lang = lang; return node; } } ================================================ FILE: Source/QuestPDF.ConformanceTests/TestEngine/VeraPdfConformanceTestRunner.cs ================================================ using System.Diagnostics; using System.Text; using System.Text.Json; using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests.TestEngine; public static class VeraPdfConformanceTestRunner { public class ValidationResult { public bool IsDocumentValid => !FailedRules.Any(); public ICollection FailedRules { get; set; } = []; public class FailedRule { public string Profile { get; set; } public string Specification { get; set; } public string Clause { get; set; } public string Description { get; set; } public string ErrorMessage { get; set; } public string Context { get; set; } } public string GetErrorMessage() { var errorMessage = new StringBuilder(); foreach (var failedRule in FailedRules) { errorMessage.AppendLine($"🟥\t{failedRule.Profile}"); errorMessage.AppendLine($"\t{failedRule.Specification}"); errorMessage.AppendLine($"\t{failedRule.Clause}"); errorMessage.AppendLine($"\t{failedRule.Description}"); errorMessage.AppendLine(); errorMessage.AppendLine($"\t{failedRule.ErrorMessage}"); errorMessage.AppendLine(); errorMessage.AppendLine($"\t{failedRule.Context}"); errorMessage.AppendLine(); } return errorMessage.ToString(); } } public static void TestConformanceWithVeraPdf(this IDocument document) { var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.pdf"); document.GeneratePdf(filePath); var result = RunVeraPDF(filePath); if (!result.IsDocumentValid) { Console.WriteLine(result.GetErrorMessage()); Assert.Fail(); } File.Delete(filePath); } public static void TestConformance(string filePath) { var result = RunVeraPDF(filePath); if (!result.IsDocumentValid) { Console.WriteLine(result.GetErrorMessage()); Assert.Fail(); } } private static ValidationResult RunVeraPDF(string pdfFilePath) { if (!File.Exists(pdfFilePath)) throw new FileNotFoundException($"PDF file not found: {pdfFilePath}"); var arguments = $"--format json \"{pdfFilePath}\""; var executablePath = Environment.GetEnvironmentVariable("VERAPDF_EXECUTABLE_PATH"); if (string.IsNullOrEmpty(executablePath)) throw new Exception("The location path of the VeraPDF executable is not set. Set the VERAPDF_EXECUTABLE_PATH environment variable to the path of the VeraPDF executable."); var process = new Process { StartInfo = new ProcessStartInfo { FileName = executablePath, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true } }; process.Start(); var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); var result = new ValidationResult(); var profileResults = JsonDocument .Parse(output) .RootElement .GetProperty("report") .GetProperty("jobs")[0] .GetProperty("validationResult"); foreach (var profileValidationResult in profileResults.EnumerateArray()) { var failedRules = profileValidationResult .GetProperty("details") .GetProperty("ruleSummaries"); foreach (var failedRule in failedRules.EnumerateArray()) { foreach (var check in failedRule.GetProperty("checks").EnumerateArray()) { result.FailedRules.Add(new ValidationResult.FailedRule { Profile = profileValidationResult.GetProperty("profileName").GetString().Split(" ").First(), Specification = failedRule.GetProperty("specification").GetString(), Clause = failedRule.GetProperty("clause").GetString(), Description = failedRule.GetProperty("description").GetString(), ErrorMessage = check.GetProperty("errorMessage").GetString(), Context = check.GetProperty("context").GetString() }); } } } return result; } } ================================================ FILE: Source/QuestPDF.ConformanceTests/TestsSetup.cs ================================================ using System.Runtime.CompilerServices; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests { public class TestsSetup { [ModuleInitializer] public static void Setup() { QuestPDF.Settings.License = LicenseType.Community; QuestPDF.Settings.UseEnvironmentFonts = false; } } } ================================================ FILE: Source/QuestPDF.ConformanceTests/ZugferdTests.cs ================================================ using QuestPDF.ConformanceTests.TestEngine; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ConformanceTests; internal class ZugferdTests { [Test] public void ZugferdValidation_WithMustang() { var guid = Guid.NewGuid(); var invoicePath = Path.Combine(Path.GetTempPath(), $"{guid}.pdf"); Document .Create(document => { document.Page(page => { page.Margin(60); page.Content() .Text("Conformance Test: ZUGFeRD") .FontSize(24) .FontColor(Colors.Blue.Darken2) .Bold(); }); }) .WithMetadata(new DocumentMetadata { Title = "Conformance Test: ZUGFeRD", Author = "SampleCompany", Subject = "ZUGFeRD Test Document", Language = "en-US" }) .WithSettings(new DocumentSettings { PDFA_Conformance = PDFA_Conformance.PDFA_3B }) .GeneratePdf(invoicePath); VeraPdfConformanceTestRunner.TestConformance(invoicePath); var zugferdInvoicePath = Path.Combine(Path.GetTempPath(), $"zugferd-{guid}.pdf"); var facturPath = Path.Combine("Resources", "zugferd-factur-x.xml"); var metadataPath = Path.Combine("Resources", "zugferd-xmp-metadata.xml"); DocumentOperation .LoadFile(invoicePath) .AddAttachment(new DocumentOperation.DocumentAttachment { Key = "factur-zugferd", FilePath = facturPath, AttachmentName = "factur-x.xml", MimeType = "text/xml", Description = "Factur-X Invoice", Relationship = DocumentOperation.DocumentAttachmentRelationship.Source, CreationDate = DateTime.UtcNow, ModificationDate = DateTime.UtcNow }) .ExtendMetadata(File.ReadAllText(metadataPath)) .Save(zugferdInvoicePath); VeraPdfConformanceTestRunner.TestConformance(zugferdInvoicePath); MustangConformanceTestRunner.TestConformance(zugferdInvoicePath); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/AccessibilityExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class AccessibilityExamples { [Test] public void MinimalExample() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.Margin(30); page.Header() .PaddingBottom(15) .SemanticHeader1() .Text("Accessibility Test Document") .FontColor(Colors.Blue.Darken3) .FontSize(24) .Bold(); page.Content() .Column(column => { column.Spacing(20); column.Item() .SemanticSection() .Column(column => { column.Item() .PaddingBottom(10) .SemanticHeader2() .Text("Section with text content") .FontColor(Colors.Blue.Darken1) .FontSize(16); column.Item() .Text(Placeholders.Paragraphs()) .FontSize(12) .ParagraphSpacing(8); }); column.Item() .PreventPageBreak() .SemanticSection() .Column(column => { column.Item() .PaddingBottom(10) .SemanticHeader2() .Text("Section with image") .FontColor(Colors.Blue.Darken1) .FontSize(16); column.Item() .Width(250) .SemanticImage("Image showing a laptop") .Image("Resources/product.jpg"); }); }); }); }) .WithMetadata(new DocumentMetadata { Language = "en-US", Title = "Accessibility Test", Subject = "This document shows how easy it is to create accessible PDF documents with QuestPDF" }) .WithSettings(new DocumentSettings { PDFA_Conformance = PDFA_Conformance.PDFA_3A, PDFUA_Conformance = PDFUA_Conformance.PDFUA_1 }) .GeneratePdf("accessibility-minimal-example.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/AlignmentExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class AlignmentExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(300) .Height(300) .AlignBottom() .AlignCenter() .Background(Colors.Grey.Lighten2) .Padding(10) .Text("Lorem ipsum"); }); }) .GenerateImages(x => "alignment.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/AspectRatioExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class AspectRatioExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(300) .Height(300) .AspectRatio(3f/4f, AspectRatioOption.FitArea) .Background(Colors.Grey.Lighten2) .AlignCenter() .AlignMiddle() .Text("3:4 Content Area"); }); }) .GenerateImages(x => "aspect-ratio.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/BackgroundExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class BackgroundExamples { [Test] public void SolidColor() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.PageColor(Colors.White); page.Margin(25); var colors = new[] { Colors.LightBlue.Darken4, Colors.LightBlue.Darken3, Colors.LightBlue.Darken2, Colors.LightBlue.Darken1, Colors.LightBlue.Medium, Colors.LightBlue.Lighten1, Colors.LightBlue.Lighten2, Colors.LightBlue.Lighten3, Colors.LightBlue.Lighten4, Colors.LightBlue.Lighten5, Colors.LightBlue.Accent1, Colors.LightBlue.Accent2, Colors.LightBlue.Accent3, Colors.LightBlue.Accent4, }; page.Content() .Height(150) .Width(420) .Row(row => { foreach (var color in colors) row.RelativeItem().Background(color); }); }); }) .GenerateImages(x => "background-solid.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void Gradient() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(350, 0)); page.MaxSize(new PageSize(350, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.PageColor(Colors.White); page.Margin(25); page.Content() .Column(column => { column.Spacing(25); column.Item() .BackgroundLinearGradient(0, [Colors.Red.Lighten2, Colors.Blue.Lighten2]) .AspectRatio(2); column.Item() .BackgroundLinearGradient(45, [Colors.Green.Lighten2, Colors.LightGreen.Lighten2, Colors.Yellow.Lighten2]) .AspectRatio(2); column.Item() .BackgroundLinearGradient(90, [Colors.Yellow.Lighten2, Colors.Amber.Lighten2, Colors.Orange.Lighten2]) .AspectRatio(2); }); }); }) .GenerateImages(x => "background-gradient.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void RoundedCorners() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.PageColor(Colors.White); page.Margin(25); page.Content() .Shrink() .Background(Colors.Grey.Lighten2) .CornerRadius(25) .Padding(25) .Text("Content with rounded corners"); }); }) .GenerateImages(x => "background-rounded-corners.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/BarcodeExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using ZXing; using ZXing.OneD; using ZXing.QrCode; using ZXing.Rendering; namespace QuestPDF.DocumentationExamples; public class BarcodeExamples { [Test] public void BarcodeExample() { Settings.UseEnvironmentFonts = false; Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(550, 0)); page.MaxSize(new PageSize(550, 1000)); page.DefaultTextStyle(x => x.FontSize(16)); page.Content() .Background(Colors.Grey.Lighten3) .Padding(25) .Row(row => { var productId = Random.Shared.NextInt64() % 10_000_000; row.Spacing(20); row.RelativeItem().Text(text => { text.ParagraphSpacing(10); text.Span("Product ID: ").Bold(); text.Line(productId.ToString("D7")); text.Span("Name: ").Bold(); text.Line(Placeholders.Label()); text.Span("Description: ").Bold(); text.Span(Placeholders.Sentence()); }); row.AutoItem() .Background(Colors.White) .AlignCenter() .AlignMiddle() .Width(200) .Height(75) .Svg(size => { var content = productId.ToString("D7"); var writer = new EAN8Writer(); var eanCode = writer.encode(content, BarcodeFormat.EAN_8, (int)size.Width, (int)size.Height); var renderer = new SvgRenderer { FontName = "Lato", FontSize = 16 }; return renderer.Render(eanCode, BarcodeFormat.EAN_8, content).Content; }); }); }); }) .GenerateImages(x => "barcode.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void QRCodeExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(550, 0)); page.MaxSize(new PageSize(550, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Background(Colors.Grey.Lighten3) .Padding(25) .Row(row => { const string url = "https://en.wikipedia.org/wiki/Algorithm"; row.Spacing(20); row.RelativeItem() .AlignMiddle() .Text(text => { text.Justify(); text.Span("In mathematics and computer science, "); text.Span("an algorithm").Bold().BackgroundColor(Colors.White); text.Span(" is a finite sequence of mathematically rigorous instructions, typically used to solve a class of specific problems or to perform a computation. "); text.Hyperlink("Learn more", url).Underline().FontColor(Colors.Blue.Darken2); }); row.ConstantItem(5, Unit.Centimetre) .AspectRatio(1) .Background(Colors.White) .Svg(size => { var writer = new QRCodeWriter(); var qrCode = writer.encode(url, BarcodeFormat.QR_CODE, (int)size.Width, (int)size.Height); var renderer = new SvgRenderer { FontName = "Lato" }; return renderer.Render(qrCode, BarcodeFormat.EAN_13, null).Content; }); }); }); }) .GenerateImages(x => "qrcode.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/BorderExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class BorderExamples { [Test] public void SimpleExample() { Document .Create(document => { document.Page(page => { page.ContinuousSize(450); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Border(3, Colors.Blue.Darken4) .Background(Colors.Blue.Lighten5) .Padding(25) .Text(text => { text.DefaultTextStyle(x => x.FontColor(Colors.Blue.Darken4).FontSize(16)); text.Span("TIP: ").Bold(); text.Span("You can use borders to create visual separation between elements in your document. Borders can be applied to any element, including text, images, and containers."); }); }); }) .GenerateImages(x => "border-simple.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void Multiple() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Shrink() .BorderVertical(5) .BorderColor(Colors.Green.Darken2) .BorderAlignmentInside() .Container() .BorderHorizontal( 10) .BorderColor(Colors.Blue.Lighten1) .BorderAlignmentInside() .Background(Colors.Grey.Lighten2) .PaddingVertical(25) .PaddingHorizontal(50) .Text("Content"); }); }) .GenerateImages(x => "border-multiple.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void ConsistentThickness() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(550, 0)); page.MaxSize(new PageSize(550, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(25); row.RelativeItem() .Border(1, Colors.Black) .Padding(10) .AlignCenter() .Text("Thin"); row.RelativeItem() .Border(3, Colors.Black) .Padding(10) .AlignCenter() .Text("Medium"); row.RelativeItem() .Border(9, Colors.Black) .Padding(10) .AlignCenter() .Text("Bold"); }); }); }) .GenerateImages(x => "border-thickness-consistent.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void VariousThickness() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .BorderLeft(4) .BorderTop(6) .BorderRight(8) .BorderBottom(10) .Padding(25) .Text("Sample text"); }); }) .GenerateImages(x => "border-thickness-various.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void Alignment() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(725, 0)); page.MaxSize(new PageSize(725, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(25); row.RelativeItem() .Background(Colors.Grey.Lighten1) .Padding(25) .Text("No Border"); row.RelativeItem() .Border(10, Colors.Grey.Darken2) .BorderAlignmentInside() .Padding(25) .Text("Border Inside"); row.RelativeItem() .Border(10, Colors.Grey.Darken2) .BorderAlignmentMiddle() .Padding(25) .Text("Border Middle"); row.RelativeItem() .Border(10, Colors.Grey.Darken2) .BorderAlignmentOutside() .Padding(25) .Text("Border Outside"); }); }); }) .GenerateImages(x => "border-alignment.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void RoundedCorners1() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .CornerRadius(10) .Border(1, Colors.Black) .Background(Colors.Grey.Lighten2) .Padding(25) .Text("Border with rounded corners"); }); }) .GenerateImages(x => "border-rounded-corners-1.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void RoundedCorners2() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .CornerRadius(10) .BorderLeft(10) .BorderAlignmentInside() .BorderColor(Colors.Green.Darken2) .Background(Colors.Green.Lighten4) .Padding(25) .PaddingLeft(10) .DefaultTextStyle(x => x.FontColor(Colors.Green.Darken4)) .Column(column => { column.Item().Text("Completed").Bold(); column.Item().Height(5); column.Item().Text("The invoice has been paid in full.").FontSize(16); }); }); }) .GenerateImages(x => "border-rounded-corners-2.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void SolidColor() { Document .Create(document => { document.Page(page => { page.ContinuousSize(450); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Row(row => { var colors = new[] { Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium }; row.Spacing(25); foreach (var color in colors) { row.RelativeItem() .Border(5) .BorderColor(color) .Padding(15) .Text(color) .FontColor(color); } }); }); }) .GenerateImages(x => "border-color-solid.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void Gradient() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Column(column => { column.Spacing(25); column.Item() .Border(5) .BorderLinearGradient(0, [Colors.Red.Darken1, Colors.Blue.Darken1]) .BorderAlignmentInside() .Padding(25) .Text("Horizontal gradient"); column.Item() .Border(10) .BorderLinearGradient(45, [Colors.Green.Darken1, Colors.LightGreen.Darken1, Colors.Yellow.Darken1]) .BorderAlignmentInside() .Padding(25) .Text("Diagonal gradient"); column.Item() .Border(10) .BorderLinearGradient(90, [Colors.Yellow.Darken1, Colors.Amber.Darken1, Colors.Orange.Darken1]) .CornerRadius(20) .Padding(25) .Text("Vertical gradient"); }); }); }) .GenerateImages(x => "border-color-gradient.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ChartExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using ScottPlot; using Colors = QuestPDF.Helpers.Colors; using ImageFormat = QuestPDF.Infrastructure.ImageFormat; namespace QuestPDF.DocumentationExamples; public class ChartExamples { [Test] public void PieChartExample() { Settings.UseEnvironmentFonts = true; Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(350, 0)); page.MaxSize(new PageSize(350, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); column.Item().Text("US energy consumption [%]\nby source in 2021").AlignCenter().Bold(); column.Item() .AspectRatio(1) .Svg(size => { using ScottPlot.Plot plot = new(); var slices = new PieSlice[] { new() { Value = 8, FillColor = new ScottPlot.Color(Colors.Yellow.Medium.Hex), Label = "Nuclear" }, new() { Value = 12, FillColor = new ScottPlot.Color(Colors.Green.Medium.Hex), Label = "Renewable" }, new() { Value = 32, FillColor = new ScottPlot.Color(Colors.Blue.Medium.Hex), Label = "Natural gas" }, new() { Value = 11, FillColor = new ScottPlot.Color(Colors.Grey.Medium.Hex), Label = "Coal" }, new() { Value = 36, FillColor = new ScottPlot.Color(Colors.Brown.Medium.Hex), Label = "Petroleum" } }; var pie = plot.Add.Pie(slices); pie.DonutFraction = 0.5; pie.SliceLabelDistance = 1.5; pie.LineColor = ScottPlot.Colors.White; pie.LineWidth = 3; foreach (var pieSlice in pie.Slices) { pieSlice.LabelStyle.FontName = "Lato"; pieSlice.LabelStyle.FontSize = 16; } plot.Axes.Frameless(); plot.HideGrid(); return plot.GetSvgXml((int)size.Width, (int)size.Height); }); }); }); }) .GenerateImages(x => "chart-pie.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void BarExample() { Settings.UseEnvironmentFonts = true; Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(650, 0)); page.MaxSize(new PageSize(650, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); column.Item().Text("Popularity of C# versions in 2023").AlignCenter().Bold(); column.Item() .AspectRatio(2) .Svg(size => { ScottPlot.Plot plot = new(); var bars = new Bar[] { new() { Position = 1, Value = 2 }, new() { Position = 2, Value = 3 }, new() { Position = 3, Value = 8 }, new() { Position = 4, Value = 13 }, new() { Position = 5, Value = 17 }, new() { Position = 6, Value = 17 }, new() { Position = 7, Value = 32 }, new() { Position = 8, Value = 42 } }; foreach (var bar in bars) { bar.FillColor = new ScottPlot.Color(Colors.Grey.Medium.Hex); bar.LineWidth = 0; bar.Size = 0.5; } plot.Add.Bars(bars); Tick[] ticks = [ new(1, "Other"), new(2, "C# 5"), new(3, "C# 6"), new(4, "C# 7"), new(5, "C# 8"), new(6, "C# 9"), new(7, "C# 10"), new(8, "C# 11") ]; plot.Axes.Bottom.TickGenerator = new ScottPlot.TickGenerators.NumericManual(ticks); plot.Axes.Bottom.MajorTickStyle.Length = 0; plot.Axes.Bottom.TickLabelStyle.FontName = "Lato"; plot.Axes.Bottom.TickLabelStyle.FontSize = 16; plot.Axes.Bottom.TickLabelStyle.OffsetY = 8; plot.Grid.XAxisStyle.IsVisible = false; plot.Axes.Margins(bottom: 0, top: 0.25f); return plot.GetSvgXml((int)size.Width, (int)size.Height); }); }); }); }) .GenerateImages(x => "chart-bars.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternAddressComponentExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternAddressComponentExample { [Test] public void Example() { var address = new Address { CompanyName = "Apple", PostalCode = "95014", Country = "United States", City = "Cupertino", Street = "One Apple Park Way" }; Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1200)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Component(new AddressComponent(address)); }); }) .GenerateImages(x => $"code-pattern-component-address.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } public class Address { public string CompanyName { get; set; } public string PostalCode { get; set; } public string Country { get; set; } public string City { get; set; } public string Street { get; set; } } public class AddressComponent : IComponent { private Address Address { get; } public AddressComponent(Address address) { Address = address; } public void Compose(IContainer container) { container .Column(column => { column.Spacing(10); AddItem("Company name", Address.CompanyName); AddItem("Postal code", Address.PostalCode); AddItem("Country", Address.Country); AddItem("City", Address.City); AddItem("Street", Address.Street); void AddItem(string label, string value) { column.Item().Text(text => { text.Span($"{label}: ").Bold(); text.Span(value); }); } }); } } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternCapturePositionExample.cs ================================================ using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternCapturePositionExample { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.ContinuousSize(575); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Background(Colors.White) .Row(row => { row.Spacing(25); row.ConstantItem(0).Dynamic(new DynamicTextSpanPositionCapture()); row.RelativeItem().CaptureContentPosition("container").Text(text => { text.Justify(); var mistakeTextStyle = TextStyle.Default .FontColor(Colors.Red.Darken3) .BackgroundColor(Colors.Red.Lighten4) .Strikethrough() .DecorationThickness(2); var correctionTextStyle = TextStyle.Default .FontColor(Colors.Green.Darken3) .BackgroundColor(Colors.Green.Lighten4); text.Span("Proofreading").Bold().Underline().DecorationThickness(2); text.Span(" technical documentation is a critical quality assurance step that ensures clarity, accuracy, and consistency across all written content. It involves more than just checking for grammar and "); text.Span("spilling").Style(mistakeTextStyle); text.Span("spelling").Style(correctionTextStyle); text.Element(TextInjectedElementAlignment.Middle).CaptureContentPosition("mistake"); text.Span(" errors—it also includes verifying terminology, code syntax, formatting standards, and logical flow. A common best practice is to have the content reviewed by both a subject matter "); text.Span("export").Style(mistakeTextStyle); text.Span("expert").Style(correctionTextStyle); text.Element(TextInjectedElementAlignment.Middle).CaptureContentPosition("mistake"); text.Span(" and a language specialist, ensuring that the material is technically sound while also being accessible to the intended audience."); }); }); }); }) .GenerateImages(x => "code-pattern-element-position-locator.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } public class DynamicTextSpanPositionCapture : IDynamicComponent { public DynamicComponentComposeResult Compose(DynamicContext context) { var containerLocation = context.GetContentCapturedPositions("container").FirstOrDefault(x => x.PageNumber == context.PageNumber); var mistakeLocations = context.GetContentCapturedPositions("mistake").Where(x => x.PageNumber == context.PageNumber).ToList(); if (containerLocation == null || mistakeLocations.Count == 0) { return new DynamicComponentComposeResult { Content = context.CreateElement(_ => { }), HasMoreContent = false }; } var content = context.CreateElement(container => { container.Layers(layers => { layers.PrimaryLayer(); foreach (var mistakeLocation in mistakeLocations) { layers .Layer() .Unconstrained() .TranslateY(mistakeLocation.Y - containerLocation.Y) .TranslateX(-12) .TranslateY(-12) .Width(24) .Svg("Resources/proofreading.svg"); } }); }); return new DynamicComponentComposeResult { Content = content, HasMoreContent = false }; } } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternComponentProgressbarComponentExample.cs ================================================ using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternComponentProgressbarComponentExample { [Test] public void Example() { var content = GenerateReport(); File.WriteAllBytes("code-pattern-dynamic-component-progressbar.pdf", content); } public byte[] GenerateReport() { return Document .Create(document => { document.Page(page => { page.Size(PageSizes.A4); page.Margin(50); page.DefaultTextStyle(x => x.FontSize(20)); page.Header().Column(column => { column.Item() .Text("MyBrick Set") .FontSize(48).FontColor(Colors.Blue.Darken2).Bold(); column.Item() .Text("Building Instruction") .FontSize(24); column.Item().Height(15); column.Item().Dynamic(new PageProgressbarComponent()); }); page.Content().PaddingVertical(25).Column(column => { column.Spacing(25); foreach (var i in Enumerable.Range(1, 30)) { column.Item() .Background(Colors.Grey.Lighten3) .Height(Random.Shared.Next(4, 8) * 25) .AlignCenter() .AlignMiddle() .Text($"Step {i}"); } }); page.Footer().Dynamic(new PageNumberSideComponent()); }); }) .GeneratePdf(); } public class PageProgressbarComponent : IDynamicComponent { public DynamicComponentComposeResult Compose(DynamicContext context) { var content = context.CreateElement(element => { var width = context.AvailableSize.Width * context.PageNumber / context.TotalPages; element .Background(Colors.Blue.Lighten3) .Height(5) .Width(width) .Background(Colors.Blue.Darken2); }); return new DynamicComponentComposeResult { Content = content, HasMoreContent = false }; } } public class PageNumberSideComponent : IDynamicComponent { public DynamicComponentComposeResult Compose(DynamicContext context) { var content = context.CreateElement(element => { element .Element(x => context.PageNumber % 2 == 0 ? x.AlignRight() : x.AlignLeft()) .Text(text => { text.Span("Page "); text.CurrentPageNumber(); }); }); return new DynamicComponentComposeResult { Content = content, HasMoreContent = false }; } } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternConfigurableComponentExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternConfigurableComponentExample { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1200)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Component(BuildSampleSection()); }); }) .GenerateImages(x => $"code-pattern-component-configurable.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); IComponent BuildSampleSection() { var section = new SectionComponent(); section.Text("Product name", Placeholders.Label()); section.Text("Description", Placeholders.Sentence()); section.Text("Price", Placeholders.Price()); section.Text("Date of production", Placeholders.ShortDate()); section.Image("Photo of the product", "Resources/product.jpg"); section.Custom("Status").Text("Accepted").FontColor(Colors.Green.Darken2).Bold(); return section; } } public class SectionComponent : IComponent { private List<(string Label, IContainer Content)> Fields { get; set; } = []; public SectionComponent() { } public void Compose(IContainer container) { container .Border(1) .Column(column => { foreach (var field in Fields) { column.Item().Row(row => { row.RelativeItem() .Border(1) .BorderColor(Colors.Grey.Medium) .Background(Colors.Grey.Lighten3) .Padding(10) .Text(field.Label); row.RelativeItem(2) .Border(1) .BorderColor(Colors.Grey.Medium) .Padding(10) .Element(field.Content); }); } }); } public void Text(string label, string text) { Custom(label).Text(text); } public void Image(string label, string imagePath) { Custom(label).Image(imagePath); } public IContainer Custom(string label) { var content = EmptyContainer.Create(); Fields.Add((label, content)); return content; } } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternContentStylingExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternContentStylingExample { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(650, 0)); page.MaxSize(new PageSize(650, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(50); columns.RelativeColumn(1); columns.RelativeColumn(2); }); table.Header(header => { header.Cell().Element(Style).Text("#"); header.Cell().Element(Style).Text("Product Name"); header.Cell().Element(Style).Text("Description"); IContainer Style(IContainer container) { return container .Background(Colors.Blue.Lighten5) .Padding(10) .DefaultTextStyle(TextStyle.Default.FontColor(Colors.Blue.Darken4).Bold()); } }); foreach (var i in Enumerable.Range(1, 5)) { table.Cell().Element(Style).Text(i.ToString()); table.Cell().Element(Style).Text(Placeholders.Label()); table.Cell().Element(Style).Text(Placeholders.Sentence()); } IContainer Style(IContainer container) { return container .BorderTop(2) .BorderColor(Colors.Blue.Lighten3) .Padding(10); } }); }); }) .GenerateImages(x => $"code-pattern-content-styling.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternDocumentStructureExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternDocumentStructureExample { [Test] public void Example() { var content = GenerateReport(); File.WriteAllBytes("code-pattern-document-structure.pdf", content); } public byte[] GenerateReport() { return Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .PaddingBottom(15) .Column(column => { column.Item().Element(ReportTitle); column.Item().PageBreak(); column.Item().Element(RedSection); column.Item().PageBreak(); column.Item().Element(GreenSection); column.Item().PageBreak(); column.Item().Element(BlueSection); }); page.Footer().AlignCenter().Text(text => text.CurrentPageNumber()); }); }) .GeneratePdf(); } private void ReportTitle(IContainer container) { container.Extend() .AlignCenter() .AlignMiddle() .Text("Multi-section report") .FontSize(48) .Bold(); } private void RedSection(IContainer container) { container.Grid(grid => { grid.Columns(3); grid.Spacing(15); grid.Item(3 ).Text("Red section") .FontColor(Colors.Red.Darken2).FontSize(32).Bold(); grid.Item(3).Text(Placeholders.Paragraph()).Light(); foreach (var i in Enumerable.Range(0, 6)) grid.Item().AspectRatio(4 / 3f).Background(Colors.Red.Lighten4); }); } private void GreenSection(IContainer container) { container.Grid(grid => { grid.Columns(3); grid.Spacing(15); grid.Item(3).Text("Green section") .FontColor(Colors.Green.Darken2).FontSize(32).Bold(); grid.Item(3).Text(Placeholders.Paragraph()).Light(); foreach (var i in Enumerable.Range(0, 12)) grid.Item().AspectRatio(4 / 3f).Background(Colors.Green.Lighten4); }); } private void BlueSection(IContainer container) { container.Grid(grid => { grid.Columns(3); grid.Spacing(15); grid.Item(3).Text("Blue section") .FontColor(Colors.Blue.Darken2).FontSize(32).Bold(); grid.Item(3).Text(Placeholders.Paragraph()).Light(); foreach (var i in Enumerable.Range(0, 18)) grid.Item().AspectRatio(4 / 3f).Background(Colors.Blue.Lighten4); }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternDynamicComponentExample.cs ================================================ using System.Globalization; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternDynamicComponentExample { [Test] public static void Dynamic() { var items = Enumerable.Range(0, 25).Select(x => new OrderItem()).ToList(); Document .Create(document => { document.Page(page => { page.Size(PageSizes.A4); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.Content() .Decoration(decoration => { decoration .Before() .PaddingBottom(10) .Text(text => { text.DefaultTextStyle(TextStyle.Default.Bold().FontColor(Colors.Blue.Darken2)); text.Span("Page "); text.CurrentPageNumber(); text.Span(" of "); text.TotalPages(); }); decoration .Content() .Dynamic(new OrdersTableWithPageSubtotalsComponent(items)); }); }); }) .GeneratePdf("code-pattern-dynamic-component-table-with-per-page-subtotals.pdf"); } public class OrderItem { public string ItemName { get; set; } = Placeholders.Label(); public int Price { get; set; } = Placeholders.Random.Next(1, 11) * 10; public int Count { get; set; } = Placeholders.Random.Next(1, 11); } public struct OrdersTableWithPageSubtotalsComponentState { public int ShownItemsCount { get; set; } } public class OrdersTableWithPageSubtotalsComponent : IDynamicComponent { private ICollection Items { get; } public OrdersTableWithPageSubtotalsComponentState State { get; set; } public OrdersTableWithPageSubtotalsComponent(ICollection items) { Items = items; State = new OrdersTableWithPageSubtotalsComponentState { ShownItemsCount = 0 }; } public DynamicComponentComposeResult Compose(DynamicContext context) { var header = ComposeHeader(context); var sampleFooter = ComposeFooter(context, []); var decorationHeight = header.Size.Height + sampleFooter.Size.Height; var rows = GetItemsForPage(context, decorationHeight).ToList(); var footer = ComposeFooter(context, rows.Select(x => x.Item)); var content = context.CreateElement(container => { container.Shrink().Decoration(decoration => { decoration.Before().Element(header); decoration.Content().Column(column => { foreach (var row in rows) column.Item().Element(row.Element); }); decoration.After().Element(footer); }); }); State = new OrdersTableWithPageSubtotalsComponentState { ShownItemsCount = State.ShownItemsCount + rows.Count }; return new DynamicComponentComposeResult { Content = content, HasMoreContent = State.ShownItemsCount < Items.Count }; } private static IDynamicElement ComposeHeader(DynamicContext context) { return context.CreateElement(element => { element .Width(context.AvailableSize.Width) .BorderBottom(1) .BorderColor(Colors.Grey.Darken2) .Padding(10) .DefaultTextStyle(TextStyle.Default.SemiBold()) .Row(row => { row.ConstantItem(50).Text("#"); row.RelativeItem().Text("Item name"); row.ConstantItem(75).AlignRight().Text("Count"); row.ConstantItem(75).AlignRight().Text("Price"); row.ConstantItem(75).AlignRight().Text("Total"); }); }); } private static IDynamicElement ComposeFooter(DynamicContext context, IEnumerable items) { var total = items.Sum(x => x.Count * x.Price); return context.CreateElement(element => { element .Width(context.AvailableSize.Width) .Padding(10) .AlignRight() .Text($"Subtotal: {total}$") .Bold(); }); } private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight) { var totalHeight = decorationHeight; foreach (var index in Enumerable.Range(State.ShownItemsCount, Items.Count - State.ShownItemsCount)) { var item = Items.ElementAt(index); var element = context.CreateElement(content => { content .Width(context.AvailableSize.Width) .BorderBottom(1) .BorderColor(Colors.Grey.Lighten2) .Padding(10) .Row(row => { row.ConstantItem(50).Text((index + 1).ToString(CultureInfo.InvariantCulture)); row.RelativeItem().Text(item.ItemName); row.ConstantItem(75).AlignRight().Text(item.Count.ToString(CultureInfo.InvariantCulture)); row.ConstantItem(75).AlignRight().Text($"{item.Price}$"); row.ConstantItem(75).AlignRight().Text($"{item.Count*item.Price}$"); }); }); var elementHeight = element.Size.Height; // it is important to use the Size.Epsilon constant to avoid floating point comparison issues if (totalHeight + elementHeight > context.AvailableSize.Height + Size.Epsilon) break; totalHeight += elementHeight; yield return (item, element); } } } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternExecutionOrderExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternExecutionOrderExample { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(400, 0)); page.MaxSize(new PageSize(400, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(25); column.Item() .Border(1) .Background(Colors.Blue.Lighten4) .Padding(15) .Text("border → background → padding"); column.Item() .Border(1) .Padding(15) .Background(Colors.Blue.Lighten4) .Text("border → padding → background"); column.Item() .Background(Colors.Blue.Lighten4) .Padding(15) .Border(1) .Text("background → padding → border"); column.Item() .Padding(15) .Border(1) .Background(Colors.Blue.Lighten4) .Text("padding → border → background"); }); }); }) .GenerateImages(x => "code-pattern-execution-order.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternExtesionMethodExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternExtensionMethodExample { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(600, 0)); page.MaxSize(new PageSize(600, 1000)); page.DefaultTextStyle(x => x.FontSize(14)); page.Margin(25); page.Content() .Border(1) .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(2); columns.RelativeColumn(3); columns.RelativeColumn(2); columns.RelativeColumn(3); }); table.Cell().TableLabelCell("Product name"); table.Cell().TableValueCell().Text(Placeholders.Label()); table.Cell().TableLabelCell("Description"); table.Cell().TableValueCell().Text(Placeholders.Sentence()); table.Cell().TableLabelCell("Price"); table.Cell().TableValueCell().Text(Placeholders.Price()); table.Cell().TableLabelCell("Date of production"); table.Cell().TableValueCell().Text(Placeholders.ShortDate()); table.Cell().ColumnSpan(2).TableLabelCell("Photo of the product"); table.Cell().ColumnSpan(2).TableValueCell().AspectRatio(16 / 9f).Image(Placeholders.Image); }); }); }) .GenerateImages(x => "code-pattern-extension-methods.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } public static class TableExtensions { private static IContainer TableCellStyle(this IContainer container, string backgroundColor) { return container .Border(1) .BorderColor(Colors.Black) .Background(backgroundColor) .Padding(10); } public static void TableLabelCell(this IContainer container, string text) { container .TableCellStyle(Colors.Grey.Lighten3) .Text(text) .Bold(); } public static IContainer TableValueCell(this IContainer container) { return container.TableCellStyle(Colors.Transparent); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternLocalHelpersExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.CodePatterns; public class CodePatternLocalHelpersExample { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(15); column.Item().Text("Business details:").FontSize(24).Bold().FontColor(Colors.Blue.Darken2); AddContactItem("Resources/Icons/phone.svg", Placeholders.PhoneNumber()); AddContactItem("Resources/Icons/email.svg", Placeholders.Email()); AddContactItem("Resources/Icons/web.svg", Placeholders.WebpageUrl()); void AddContactItem(string iconPath, string label) { column.Item().Row(row => { row.ConstantItem(32).AspectRatio(1).Svg(iconPath); row.ConstantItem(15); row.AutoItem().AlignMiddle().Text(label); }); } }); }); }) .GenerateImages(x => $"code-pattern-local-helpers.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ColorsExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ColorsExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(175) .Padding(20) .Border(1) .BorderColor("#03A9F4") .Background(Colors.LightBlue.Lighten5) .Padding(20) .Text("Blue text") .Bold() .FontColor(Colors.LightBlue.Darken4) .Underline() .DecorationWavy() .DecorationColor(0xFF0000); }); }) .GenerateImages(x => "colors.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ColumnExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ColumnExamples { [Test] public void SimpleExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(250, 0)); page.MaxSize(new PageSize(250, 1000)); page.Margin(25); page.Content() .Column(column => { column.Item().Background(Colors.Grey.Medium).Height(50); column.Item().Background(Colors.Grey.Lighten1).Height(75); column.Item().Background(Colors.Grey.Lighten2).Height(100); }); }); }) .GenerateImages(x => "column-simple.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void SpacingExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(250, 0)); page.MaxSize(new PageSize(250, 1000)); page.Margin(25); page.Content() .Column(column => { column.Spacing(25); column.Item().Background(Colors.Grey.Medium).Height(50); column.Item().Background(Colors.Grey.Lighten1).Height(75); column.Item().Background(Colors.Grey.Lighten2).Height(100); }); }); }) .GenerateImages(x => "column-spacing.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void CustomSpacingExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(250, 0)); page.MaxSize(new PageSize(250, 1000)); page.Margin(25); page.Content() .Column(column => { column.Item().Background(Colors.Grey.Darken1).Height(50); column.Item().Height(10); column.Item().Background(Colors.Grey.Medium).Height(50); column.Item().Height(20); column.Item().Background(Colors.Grey.Lighten1).Height(50); column.Item().Height(30); column.Item().Background(Colors.Grey.Lighten2).Height(50); }); }); }) .GenerateImages(x => "column-spacing-custom.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void DisableUniformItemsWidthExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(400, 0)); page.MaxSize(new PageSize(400, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Column(column => { column.Spacing(15); column.Item() .Element(LabelStyle) .Text("REST API"); column.Item() .Element(LabelStyle) .Text("Garbage Collection"); column.Item() .Element(LabelStyle) .Text("Object-Oriented Programming"); static IContainer LabelStyle(IContainer container) => container .ShrinkHorizontal() .Background(Colors.Grey.Lighten3) .CornerRadius(15) .Padding(15); }); }); }) .GenerateImages(x => "column-uniform-width-disabled.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ComplexGraphicsExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ComplexGraphicsExamples { [Test] public void RoundedRectangleWithGradient() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Layers(layers => { layers.Layer().Svg(size => { return $""" """; }); layers.PrimaryLayer() .PaddingVertical(10) .PaddingHorizontal(20) .Text("QuestPDF") .FontColor(Colors.White) .FontSize(32) .ExtraBlack(); }); }); }) .GenerateImages(x => "complex-graphics-rounded-rectangle-with-gradient.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void DottedLine() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(500, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(5); foreach (var i in Enumerable.Range(1, 5)) { var pageNumber = i * 7 + 4; column.Item().Row(row => { row.AutoItem().Text($"{i}."); row.ConstantItem(10); row.AutoItem().Text(Placeholders.Label()); row.RelativeItem().PaddingHorizontal(3).TranslateY(20).Height(2).Svg(size => { return $""" """; }); row.AutoItem().Text($"{pageNumber}"); }); } }); }); }) .GenerateImages(x => "complex-graphics-dotted-line.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ConstrainedExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ConstrainedExamples { [Test] public void WidthExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(300) .Padding(25) .Column(column => { column.Spacing(25); column.Item() .MinWidth(200) .Background(Colors.Grey.Lighten3) .Text("Lorem ipsum"); column.Item() .MaxWidth(100) .Background(Colors.Grey.Lighten3) .Text("dolor sit amet"); }); }); }) .GenerateImages(x => "width.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void HeightExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(300) .Padding(25) .Height(100) .AspectRatio(2f, AspectRatioOption.FitHeight) .Background(Colors.Grey.Lighten1); }); }) .GenerateImages(x => "height.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ContentDirectionExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ContentDirectionExamples { [Test] public void LeftToRightExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(250) .ContentFromLeftToRight() .Row(row => { row.Spacing(5); row.AutoItem().Height(50).Width(50).Background(Colors.Red.Lighten1); row.AutoItem().Height(50).Width(50).Background(Colors.Green.Lighten1); row.AutoItem().Height(50).Width(75).Background(Colors.Blue.Lighten1); }); }); }) .GenerateImages(x => "content-direction-ltr.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void RightToLeftExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(250) .ContentFromRightToLeft() .Row(row => { row.Spacing(5); row.AutoItem().Height(50).Width(50).Background(Colors.Red.Lighten1); row.AutoItem().Height(50).Width(50).Background(Colors.Green.Lighten1); row.AutoItem().Height(50).Width(75).Background(Colors.Blue.Lighten1); }); }); }) .GenerateImages(x => "content-direction-rtl.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/CustomFirstPageExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class CustomFirstPageExample { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.Margin(30); page.DefaultTextStyle(x => x.FontSize(20)); page.Header().Column(column => { column.Item().ShowOnce().Background(Colors.Blue.Lighten2).Height(80); column.Item().SkipOnce().Background(Colors.Green.Lighten2).Height(60); }); page.Content().PaddingVertical(20).Column(column => { column.Spacing(20); foreach (var _ in Enumerable.Range(0, 20)) column.Item().Background(Colors.Grey.Lighten3).Height(40); }); page.Footer().AlignCenter().Text(text => { text.CurrentPageNumber(); text.Span(" / "); text.TotalPages(); }); }); }) .GeneratePdf("example-custom-first-page.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/DebugAreaExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class DebugAreaExamples { [Test] public void LeftToRightExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(250) .Height(250) .Padding(25) .DebugArea("Grid example", Colors.Blue.Medium) .Grid(grid => { grid.Columns(3); grid.Spacing(5); foreach (var _ in Enumerable.Range(0, 8)) grid.Item().Height(50).Placeholder(); }); }); }) .GenerateImages(x => "debug-area.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 216 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/DecorationExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class DecorationExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(350, 0)); page.MaxSize(new PageSize(350, 300)); page.Margin(25); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Background(Colors.Grey.Lighten3) .Padding(15) .Decoration(decoration => { decoration .Before() .DefaultTextStyle(x => x.Bold()) .Column(column => { column.Item().ShowOnce().Text("Customer Instructions:"); column.Item().SkipOnce().Text("Customer Instructions [continued]:"); }); decoration .Content() .PaddingTop(10) .Text("Please wrap the item in elegant gift paper and include a small blank card for a personal message. If possible, remove any price tags or invoices from the package. Make sure the wrapping is secure but easy to open without damaging the contents."); }); }); }) .GenerateImages(x => $"decoration-{x}.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/DefaultTextStyleExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class DefaultTextStyleExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(400) .Padding(25) .DefaultTextStyle(x => x.Bold().Underline()) .Column(column => { column.Spacing(10); column.Item().Text("Inherited bold and underline"); column.Item().Text("Disabled underline, inherited bold and adjusted font color").Underline(false).FontColor(Colors.Green.Darken2); column.Item() .DefaultTextStyle(x => x.DecorationWavy().FontColor(Colors.LightBlue.Darken3)) .Text("Changed underline type and adjusted font color"); }); }); }) .GenerateImages(x => "default-text-style.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/DocumentOperationExamples.cs ================================================ using System.Runtime.InteropServices; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; [TestFixture] public class DocumentOperationExamples { [Test] public void MergeFiles() { const string prefix = "document-operation-merge"; GenerateSampleDocument($"{prefix}-source-red.pdf", Colors.Red.Lighten3, 2); GenerateSampleDocument($"{prefix}-source-green.pdf", Colors.Green.Lighten3, 3); GenerateSampleDocument($"{prefix}-source-blue.pdf", Colors.Blue.Lighten3, 5); DocumentOperation .LoadFile($"{prefix}-source-red.pdf") .MergeFile($"{prefix}-source-green.pdf") .MergeFile($"{prefix}-source-blue.pdf") .Save($"{prefix}-result.pdf"); } [Test] public void SelectEvenPages() { const string prefix = "document-operation-select-even-pages"; GenerateSampleDocument($"{prefix}-source.pdf", Colors.Indigo.Lighten3, 11); DocumentOperation .LoadFile($"{prefix}-source.pdf") .TakePages("1-z:even") .Save($"{prefix}-result.pdf"); } [Test] public void Encrypt() { const string prefix = "document-operation-encrypt"; GenerateSampleDocument($"{prefix}-source.pdf", Colors.Orange.Lighten3, 7); DocumentOperation .LoadFile($"{prefix}-source.pdf") .Encrypt(new DocumentOperation.Encryption256Bit() { UserPassword = "user-password", OwnerPassword = "owner-password", AllowContentExtraction = false, AllowPrinting = false }) .Save($"{prefix}-result.pdf"); } [Test] public void AddAttachment() { const string prefix = "document-operation-add-attachment"; GenerateSampleDocument($"{prefix}-source.pdf", Colors.Cyan.Lighten3, 7); File.WriteAllText($"{prefix}-content.txt", "Hello, World!"); DocumentOperation .LoadFile($"{prefix}-source.pdf") .AddAttachment(new DocumentOperation.DocumentAttachment { FilePath = $"{prefix}-content.txt", AttachmentName = "Attached message" }) .Save($"{prefix}-result.pdf"); } [Test] public void Overlay() { const string prefix = "document-operation-overlay"; GenerateSampleDocument($"{prefix}-source.pdf", Colors.Cyan.Lighten3, 7); Document .Create(document => { document.Page(page => { page.Margin(1, Unit.Centimetre); page.PageColor(Colors.Transparent); page.Content().Column(column => { foreach (var i in Enumerable.Range(0, 6)) column.Item().PageBreak(); }); page.Footer().AlignCenter().Text(text => { text.DefaultTextStyle(x => x.FontSize(24).Bold().FontColor(Colors.White)); text.Span("Page "); text.CurrentPageNumber(); text.Span(" of "); text.TotalPages(); }); }); }) .GeneratePdf($"{prefix}-content.pdf"); DocumentOperation .LoadFile($"{prefix}-source.pdf") .OverlayFile(new DocumentOperation.LayerConfiguration { FilePath = $"{prefix}-content.pdf" }) .Save($"{prefix}-result.pdf"); } private void GenerateSampleDocument(string fileName, Color pageColor, int numberOfPages) { Document .Create(container => { container.Page(page => { page.Margin(1, Unit.Centimetre); page.PageColor(pageColor); page.Content().Column(column => { foreach (var pageNumber in Enumerable.Range(1, numberOfPages)) { column.Item() .Extend() .AlignCenter().AlignMiddle() .Text($"{pageNumber}") .FontSize(256) .FontColor(Colors.White) .Bold(); if (pageNumber != numberOfPages) column.Item().PageBreak(); } }); }); }) .GeneratePdf(fileName); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/EnsureSpaceExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class EnsureSpaceExamples { [Test] public void EnabledExample() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(30); page.Content() .Column(column => { column.Item().Height(400).Background(Colors.Grey.Lighten3); column.Item().Height(30); column.Item() .EnsureSpace(100) .Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(40); columns.RelativeColumn(); }); foreach (var i in Enumerable.Range(1, 12)) { table.Cell().Text($"{i}."); table.Cell().ShowEntire().Text(Placeholders.Sentence()); } }); }); }); }) .GeneratePdf("ensure-space-enabled.pdf"); } [Test] public void DisabledExample() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(30); page.Content() .Column(column => { column.Item().Height(400).Background(Colors.Grey.Lighten3); column.Item().Height(30); column.Item() .Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(40); columns.RelativeColumn(); }); foreach (var i in Enumerable.Range(1, 12)) { table.Cell().Text($"{i}."); table.Cell().Text(Placeholders.Sentence()); } }); }); }); }) .GeneratePdf("ensure-space-disabled.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/FlipExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class FlipExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(350, 0)); page.MaxSize(new PageSize(350, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(15); column.Item() .Text("Read the message below by putting a mirror on the right side of the screen."); column.Item() .AlignLeft() .Background(Colors.Red.Lighten5) .Padding(10) .FlipHorizontal() .Text("This is a secret message.") .FontColor(Colors.Red.Darken2); }); }); }) .GenerateImages(x => "flip.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/HyperlinkExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class HyperlinkExamples { [Test] public void ElementExample() { Document .Create(document => { document.Page(page => { page.ContinuousSize(400); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(25); column.Item() .Text("Clicking the NuGet logo will redirect you to the NuGet website."); column.Item() .Width(150) .Hyperlink("https://www.nuget.org/") .Svg("Resources/nuget-logo.svg"); }); }); }) .GeneratePdf("hyperlink-element.pdf"); } [Test] public void InsideTextExample() { Document .Create(document => { document.Page(page => { page.ContinuousSize(300); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("Click "); text.Hyperlink("here", "https://www.nuget.org/").Underline().FontColor(Colors.Blue.Darken2); text.Span(" to visit the official NuGet website."); }); }); }) .GeneratePdf("hyperlink-text.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ImageExamples.cs ================================================ using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using SkiaSharp; namespace QuestPDF.DocumentationExamples; public class ImageExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(400, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Grid(grid => { grid.Columns(2); grid.Spacing(10); grid.Item(2).Text("My photo gallery:").Bold(); grid.Item().Image("Resources/Photos/photo-gallery-1.jpg"); grid.Item().Image("Resources/Photos/photo-gallery-2.jpg"); grid.Item().Image("Resources/Photos/photo-gallery-3.jpg"); grid.Item().Image("Resources/Photos/photo-gallery-4.jpg"); }); }); }) .GenerateImages(x => "image-example.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void ImageScaling() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1500)); page.Margin(25); page.Content() .Column(column => { column.Item().PaddingBottom(5).Text("FitWidth").Bold(); column.Item() .Width(200) .Height(150) .Border(4) .BorderColor(Colors.Red.Medium) .Image("Resources/Photos/photo.jpg") .FitWidth(); column.Item().Height(15); column.Item().PaddingBottom(5).Text("FitHeight").Bold(); column.Item() .Width(200) .Height(100) .Border(4) .BorderColor(Colors.Red.Medium) .Image("Resources/Photos/photo.jpg") .FitHeight(); column.Item().Height(15); column.Item().PaddingBottom(5).Text("FitArea 1").Bold(); column.Item() .Width(200) .Height(100) .Border(4) .BorderColor(Colors.Red.Medium) .Image("Resources/Photos/photo.jpg") .FitArea(); column.Item().Height(15); column.Item().PaddingBottom(5).Text("FitArea 2").Bold(); column.Item() .Width(200) .Height(150) .Border(4) .BorderColor(Colors.Red.Medium) .Image("Resources/Photos/photo.jpg") .FitArea(); column.Item().Height(15); column.Item().PaddingBottom(5).Text("FitUnproportionally").Bold(); column.Item() .Width(200) .Height(50) .Border(4) .BorderColor(Colors.Red.Medium) .Image("Resources/Photos/photo.jpg") .FitUnproportionally(); }); }); }) .GenerateImages(x => "image-scaling.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void DpiSetting() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(400, 1000)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); // lower raster dpi = lower resolution, pixelation column .Item() .Image("Resources/Photos/photo.jpg") .WithRasterDpi(16); // higher raster dpi = higher resolution column .Item() .Image("Resources/Photos/photo.jpg") .WithRasterDpi(288); }); }); }) .GenerateImages(x => "image-dpi.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void CompressionSetting() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(400, 1000)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); // low quality = smaller output file column .Item() .Image("Resources/Photos/photo.jpg") .WithCompressionQuality(ImageCompressionQuality.VeryLow); // high quality / fidelity = larger output file column .Item() .Image("Resources/Photos/photo.jpg") .WithCompressionQuality(ImageCompressionQuality.VeryHigh); }); }); }) .GenerateImages(x => "image-compression.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void GlobalSettings() { Document .Create(document => { document.Page(page => { page.Content().Image("Resources/Photos/photo.jpg"); }); }) .WithSettings(new DocumentSettings { // default: ImageCompressionQuality.High; ImageCompressionQuality = ImageCompressionQuality.Medium, // default: 288 ImageRasterDpi = 14 }) .GeneratePdf("image-global-settings.pdf"); } [Test] public void SharedImages() { using var image = Image.FromFile("Resources/checkbox.png"); Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(350, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(15); foreach (var i in Enumerable.Range(0, 5)) { column.Item().Row(row => { row.AutoItem().Width(28).Image(image); row.RelativeItem().PaddingLeft(8).AlignMiddle().Text(Placeholders.Label()); }); } }); }); }) .GenerateImages(x => "image-shared.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void DynamicImage() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(350, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.PageColor(Colors.Grey.Lighten3); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); column.Item().Text(text => { text.Span("The national flag of Poland").Bold(); text.Span(" consists of two horizontal stripes of equal width, the upper one white and the lower one red."); }); column.Item() .AspectRatio(80 / 50f) .Border(2) .Image(GenerateNationalFlagOfPoland); }); byte[]? GenerateNationalFlagOfPoland(GenerateDynamicImageDelegatePayload context) { using var whitePaint = new SKPaint { Color = SKColors.White, }; using var redPaint = new SKPaint { Color = SKColor.Parse("#BB0A30"), }; using var bitmap = new SKBitmap(context.ImageSize.Width, context.ImageSize.Height); using var canvas = new SKCanvas(bitmap); canvas.DrawRect(0, 0, context.ImageSize.Width, context.ImageSize.Height / 2, whitePaint); canvas.DrawRect(0, context.ImageSize.Height / 2, context.ImageSize.Width, context.ImageSize.Height, redPaint); canvas.Flush(); using var content = bitmap.Encode(SKEncodedImageFormat.Png, 100); return content.ToArray(); } }); }) .GenerateImages(x => "image-dynamic.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void SvgSupport() { Document .Create(document => { document.Page(page => { page.ContinuousSize(250); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); var svgContent = File.ReadAllText("Resources/pdf-icon.svg"); page.Content() .Column(column => { column.Item().Text("The classic PDF icon looks like this:").Bold(); column.Item().Height(15); column.Item().Svg(svgContent); }); }); }) .GenerateImages(x => "image-svg.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/InlinedExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class InlinedExamples { [Test] public void SimpleExample() { Document .Create(document => { document.Page(page => { page.ContinuousSize(450); page.Content() .Background(Colors.Grey.Lighten3) .Padding(25) .Border(1) .Background(Colors.White) .Inlined(inlined => { inlined.Spacing(25); inlined.BaselineMiddle(); inlined.AlignCenter(); foreach (var _ in Enumerable.Range(0, 15)) inlined.Item().Element(RandomBlock); }); }); }) .GenerateImages(x => "inlined.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void SpacingExample() { Document .Create(document => { document.Page(page => { page.ContinuousSize(450); page.Content() .Background(Colors.Grey.Lighten3) .Padding(25) .Border(1) .Background(Colors.White) .Inlined(inlined => { inlined.VerticalSpacing(15); inlined.HorizontalSpacing(30); foreach (var _ in Enumerable.Range(0, 10)) inlined.Item().Element(RandomBlock); }); }); }) .GenerateImages(x => "inlined-spacing.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } private void RandomBlock(IContainer container) { container .Width(Random.Shared.Next(1, 4) * 25) .Height(Random.Shared.Next(1, 4) * 25) .Border(1) .BorderColor(Colors.Grey.Darken2) .Background(Placeholders.BackgroundColor()); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/LayersExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class LayersExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.ContinuousSize(450); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Item().PaddingBottom(15).Text("Proposed Business Card Design:").Bold(); column.Item() .AspectRatio(4 / 3f) .Layers(layers => { layers.Layer().Image("Resources/card-background.jpg").FitUnproportionally(); layers.PrimaryLayer() .TranslateY(75) .Column(innerColumn => { innerColumn.Item() .AlignCenter() .Text("Horizon Ventures") .Bold().FontSize(32).FontColor(Colors.Blue.Darken2); innerColumn.Item().AlignCenter().Text("Your journey begins here"); }); }); }); }); }) .GenerateImages(x => "layers.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/LazyExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class LazyExamples { class SimpleComponent : IComponent { public required int Start { get; init; } public required int End { get; init; } public void Compose(IContainer container) { container.Decoration(decoration => { decoration.Before() .Text($"Numbers from {Start} to {End}") .FontSize(20).Bold().FontColor(Colors.Blue.Darken2); decoration.Content().Column(column => { foreach (var i in Enumerable.Range(Start, End - Start + 1)) column.Item().Text($"Number {i}").FontSize(10); }); }); } } [Test] [Ignore("This test is for manual testing only.")] public void Disabled() { Document .Create(document => { document.Page(page => { page.Margin(10); page.Content().Column(column => { const int sectionSize = 1000; foreach (var i in Enumerable.Range(0, 1000)) { column.Item().Component(new SimpleComponent { Start = i * sectionSize, End = i * sectionSize + sectionSize - 1 }); } }); }); }) .GeneratePdf("lazy-disabled.pdf"); } [Test] [Ignore("This test is for manual testing only.")] public void Enabled() { Document .Create(document => { document.Page(page => { page.Margin(10); page.Content().Column(column => { const int sectionSize = 1000; foreach (var i in Enumerable.Range(0, 1000)) { var start = i * sectionSize; var end = start + sectionSize - 1; column.Item().Lazy(c => { c.Component(new SimpleComponent { Start = start, End = end }); }); } }); }); }) .GeneratePdf("lazy-enabled.pdf"); } [Test] [Ignore("This test is for manual testing only.")] public void EnabledWithCache() { Document .Create(document => { document.Page(page => { page.Margin(10); page.Content().Column(column => { const int sectionSize = 1000; foreach (var i in Enumerable.Range(0, 1000)) { var start = i * sectionSize; var end = start + sectionSize - 1; column.Item().LazyWithCache(c => { c.Component(new SimpleComponent { Start = start, End = end }); }); } }); }); }) .GeneratePdf("lazy-enabled-with-cache.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/LicenseSetup.cs ================================================ using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; [SetUpFixture] public class LicenseSetup { [OneTimeSetUp] public static void Setup() { QuestPDF.Settings.License = LicenseType.Community; } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/LineExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class LineExamples { [Test] public void VerticalLineExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Row(row => { row.AutoItem().Text("Text on the left"); row.AutoItem() .PaddingHorizontal(15) .LineVertical(3) .LineColor(Colors.Blue.Medium); row.AutoItem().Text("Text on the right"); }); }); }) .GenerateImages(x => "line-vertical.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void HorizontalLineExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Column(column => { column.Item().Text("Text above the line"); column.Item() .PaddingVertical(10) .LineHorizontal(2) .LineColor(Colors.Blue.Medium); column.Item().Text("Text below the line"); }); }); }) .GenerateImages(x => "line-horizontal.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Thickness() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Column(column => { column.Spacing(20); foreach (var thickness in new[] { 1, 2, 4, 8 }) { column.Item() .Width(200) .LineHorizontal(thickness); } }); }); }) .GenerateImages(x => "line-thickness.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void SolidColor() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Column(column => { var colors = new[] { Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium, }; column.Spacing(20); foreach (var color in colors) { column.Item() .Width(200) .LineHorizontal(5) .LineColor(color); } }); }); }) .GenerateImages(x => "line-color-solid.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Gradient() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Column(column => { column.Spacing(20); column.Item() .Width(200) .LineHorizontal(5) .LineGradient([Colors.Red.Medium, Colors.Orange.Medium]); column.Item() .Width(200) .LineHorizontal(5) .LineGradient([Colors.Orange.Medium, Colors.Yellow.Medium, Colors.Lime.Medium]); column.Item() .Width(200) .LineHorizontal(5) .LineGradient([Colors.Blue.Lighten2, Colors.LightBlue.Lighten1, Colors.Cyan.Medium, Colors.Teal.Darken1, Colors.Green.Darken2]); }); }); }) .GenerateImages(x => "line-color-gradient.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void DashPattern() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Column(column => { column.Spacing(20); column.Item() .Width(200) .LineHorizontal(5) .LineDashPattern([4f, 4f]); column.Item() .Width(200) .LineHorizontal(5) .LineDashPattern([12f, 12f]); column.Item() .Width(200) .LineHorizontal(5) .LineDashPattern([4f, 4f, 12f, 4f]); }); }); }) .GenerateImages(x => "line-dash-pattern.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Complex() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Width(300) .LineHorizontal(8) .LineDashPattern([4, 4, 8, 8, 12, 12]) .LineGradient([Colors.Red.Medium, Colors.Orange.Medium, Colors.Yellow.Medium]); }); }) .GenerateImages(x => "line-example.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ListExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ListExamples { [Test] public void BulletpointExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(350, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); foreach (var i in Enumerable.Range(1, 7)) { column.Item().Row(row => { row.ConstantItem(26).Image("Resources/bulletpoint.png"); row.ConstantItem(5); row.RelativeItem().Text(Placeholders.Label()); }); } }); }); }) .GenerateImages(x => "list-unordered.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void OrderedExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); foreach (var i in Enumerable.Range(1, 11)) { column.Item().Row(row => { row.ConstantItem(35).Text($"{i}."); row.RelativeItem().Text(Placeholders.Sentence()); }); } }); }); }) .GenerateImages(x => "list-ordered.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Nested() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { const float nestingSize = 25; column.Spacing(10); column.Item() .Text("Algorithm: Checking if a Number is Prime") .FontSize(24).FontColor(Colors.Blue.Darken2); AddListItem(0, "1.", "Handle special cases"); AddListItem(1, "a)", "If n is less than 2, return false (not prime)."); AddListItem(1, "b)", "If n is 2, return true (prime)."); AddListItem(0, "2.", "Check divisibility"); AddListItem(1, "-", "Iterate through numbers from 2 to n - 1:"); AddListItem(2, "-", "If n is divisible by any of these numbers, return false."); AddListItem(0, "3.", "Return true (if no divisors were found, n is prime)."); void AddListItem(int nestingLevel, string bulletText, string text) { column.Item().Row(row => { row.ConstantItem(nestingSize * nestingLevel); row.ConstantItem(nestingSize).Text(bulletText); row.RelativeItem().Text(text); }); } }); }); }) .GenerateImages(x => "list-nested.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/MapExample.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; static class MapboxStaticMapRenderer { private const string MapboxBaseUrl = "https://api.mapbox.com/styles/v1/mapbox/streets-v12/static"; private const string AccessToken = "pk.eyJ1IjoibWFyY2luLXppYWJlayIsImEiOiJjbTc5cHZkZTUwNmM4MmxxdGN2cnRxMTBpIn0.8G-_nwFqjjfNQUCmHSOqKw"; public static async Task FetchStaticMapAsync(double longitude, double latitude, float zoom, int width, int height) { var longitudeString = longitude.ToString(System.Globalization.CultureInfo.InvariantCulture); var latitudeString = latitude.ToString(System.Globalization.CultureInfo.InvariantCulture); var url = $"{MapboxBaseUrl}/{longitudeString},{latitudeString},{zoom},0,0/{width}x{height}@2x?access_token={AccessToken}"; using var client = new HttpClient(); try { var response = await client.GetAsync(url); return await response.Content.ReadAsByteArrayAsync(); } catch (Exception ex) { return null; } } } public class MapExample { [Test] public async Task SimpleExample() { var map = await MapboxStaticMapRenderer.FetchStaticMapAsync(19.9376052f, 50.0616087f, 10, 500, 400); Document .Create(document => { document.Page(page => { page.ContinuousSize(550); page.Margin(25); page.Content() .Column(column => { column.Item().Text("Map of Kraków").FontSize(20).Bold(); column.Item().Text("Capital of Lesser Poland Voivodeship").FontSize(16).Light(); column.Item().Height(15); column.Item() .Background(Colors.Grey.Lighten3) .ShowIf(map != null) .Image(map); }); }); }) .GenerateImages(x => "map.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/MergingDocumentsExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class MergingDocumentsExamples { [Test] public async Task UseOriginalPageNumbersExample() { Document .Merge( GenerateReport("Short Document 1", 5), GenerateReport("Medium Document 2", 10), GenerateReport("Long Document 3", 15)) .UseOriginalPageNumbers() .GeneratePdf("merged.pdf"); } [Test] public async Task UseContinuousPageNumbersExample() { Document .Merge( GenerateReport("Short Document 1", 5), GenerateReport("Medium Document 2", 10), GenerateReport("Long Document 3", 15)) .UseContinuousPageNumbers() .GeneratePdf("merged.pdf"); } #region Example document private static Document GenerateReport(string title, int itemsCount) { return Document.Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.Margin(0.5f, Unit.Inch); page.Header() .Text(title) .Bold() .FontSize(24) .FontColor(Colors.Blue.Accent2); page.Content() .PaddingVertical(20) .Column(column => { column.Spacing(10); foreach (var i in Enumerable.Range(0, itemsCount)) { column .Item() .Width(200) .Height(50) .Background(Colors.Grey.Lighten3) .AlignMiddle() .AlignCenter() .Text($"Item {i}") .FontSize(16); } }); page.Footer() .AlignCenter() .PaddingVertical(20) .Text(text => { text.DefaultTextStyle(TextStyle.Default.FontSize(16)); text.CurrentPageNumber(); text.Span(" / "); text.TotalPages(); }); }); }); } #endregion } ================================================ FILE: Source/QuestPDF.DocumentationExamples/MultiColumnExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class MultiColumnExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(650, 0)); page.MaxSize(new PageSize(650, 650)); page.DefaultTextStyle(x => x.FontSize(12)); page.Margin(25); page.Content() .MultiColumn(multiColumn => { multiColumn.Columns(3); multiColumn.Spacing(25); multiColumn .Content() .Column(column => { column.Spacing(15); foreach (var sectionId in Enumerable.Range(0, 3)) { foreach (var textId in Enumerable.Range(0, 3)) column.Item().Text(Placeholders.Paragraph()).Justify(); column.Item().AspectRatio(21 / 9f).Image(Placeholders.Image); } }); }); }); }) .GenerateImages(x => "multicolumn-example.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } [Test] public void SpacerExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(450, 0)); page.MaxSize(new PageSize(450, 550)); page.DefaultTextStyle(x => x.FontSize(12)); page.Margin(25); page.Content() .MultiColumn(multiColumn => { multiColumn.Columns(2); multiColumn.Spacing(50); multiColumn .Spacer() .AlignCenter() .LineVertical(2) .LineColor(Colors.Grey.Medium); multiColumn .Content() .Column(column => { column.Spacing(15); foreach (var textId in Enumerable.Range(0, 5)) column.Item().Text(Placeholders.Paragraph()).Justify(); }); }); }); }) .GenerateImages(x => "multicolumn-spacer.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } [Test] public void BalanceHeightWithExample() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A4); page.DefaultTextStyle(x => x.FontSize(14)); page.Margin(30); page.Content() .MultiColumn(multiColumn => { multiColumn.Spacing(30); multiColumn.BalanceHeight(); multiColumn .Content() .Column(column => { column.Spacing(15); foreach (var textId in Enumerable.Range(0, 8)) column.Item().Text(Placeholders.Paragraph()).Justify(); }); }); }); }) .GenerateImages(x => "multicolumn-balance-height-with.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/PaddingExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class PaddingExamples { [Test] public void SimpleExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(250) .PaddingVertical(10) .PaddingLeft(20) .PaddingRight(40) .Background(Colors.Grey.Lighten2) .Text("Sample text"); }); }) .GenerateImages(x => "padding-simple.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void NegativeExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(250) .Padding(50) .Background(Colors.Grey.Lighten2) .PaddingHorizontal(-25) .Text("Sample text with negative padding"); }); }) .GenerateImages(x => "padding-negative.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/PageBreakExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class PageBreakExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.Size(300, 450); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .PaddingTop(15) .Column(column => { var terms = new[] { ("Garbage Collection", "An automatic memory management feature in many programming languages that identifies and removes unused objects to free up memory, preventing memory leaks."), ("Constructor", "A special method in object-oriented programming that is automatically called when an object is created. It initializes the object's properties and sets up any necessary resources."), ("Dependency", "A software component or external library that a program relies on to function correctly. Dependencies can include third-party modules, frameworks, or system-level packages that provide additional functionality without requiring developers to write everything from scratch.") }; column.Item() .Extend() .AlignCenter().AlignMiddle() .Text("Programming dictionary").FontSize(24).Bold(); foreach (var term in terms) { column.Item().PageBreak(); column.Item().Element(c => GeneratePage(c, term.Item1, term.Item2)); } static void GeneratePage(IContainer container, string term, string definition) { container.Text(text => { text.Span(term).Bold().FontColor(Colors.Blue.Darken2); text.Span($" - {definition}"); }); } }); }); }) .GeneratePdf("page-break.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/PageExamples.cs ================================================ using QuestPDF.Companion; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class PageExamples { [Test] public void Simple() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.Margin(2, Unit.Centimetre); page.DefaultTextStyle(x => x.FontSize(24)); page.Header() .Text("Hello, World!") .FontSize(48).Bold(); page.Content() .PaddingVertical(25) .Text(Placeholders.LoremIpsum()) .Justify(); page.Footer() .AlignCenter() .Text(text => { text.CurrentPageNumber(); text.Span(" / "); text.TotalPages(); }); }); }) .GenerateImages(x => "page-simple.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void MainSlots() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A4); page.Margin(2, Unit.Centimetre); page.DefaultTextStyle(x => x.FontSize(24)); page.Header() .Background(Colors.Grey.Lighten1) .Height(125) .AlignCenter() .AlignMiddle() .Text("Header"); page.Content() .Background(Colors.Grey.Lighten2) .AlignCenter() .AlignMiddle() .Text("Content"); page.Footer() .Background(Colors.Grey.Lighten1) .Height(75) .AlignCenter() .AlignMiddle() .Text("Footer"); }); }) .GenerateImages(x => "page-main-slots.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void Foreground() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A4); page.Margin(2, Unit.Centimetre); page.DefaultTextStyle(x => x.FontSize(20)); page.Header() .PaddingBottom(1, Unit.Centimetre) .Text("Report") .FontSize(30) .Bold(); page.Content() .Text(Placeholders.Paragraphs()) .ParagraphSpacing(1, Unit.Centimetre) .Justify(); page.Foreground().Svg("Resources/draft-foreground.svg").FitArea(); }); }) .GenerateImages(x => "page-foreground.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } [Test] public void Background() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A4.Landscape()); page.Background().Svg("Resources/certificate-background.svg").FitArea(); page.Content() .PaddingLeft(10, Unit.Centimetre) .PaddingRight(5 , Unit.Centimetre) .AlignMiddle() .Column(column => { column.Item().Height(50).Svg("Resources/questpdf-logo.svg"); column.Item().Height(50); column.Item().Text("CERTIFICATE").FontSize(64).ExtraBlack(); column.Item().Height(25); column.Item() .Shrink().BorderBottom(1).Padding(10) .Text("Marcin Ziąbek").FontSize(32).Italic(); column.Item().Height(10); column.Item() .Text($"has successfully completed the course \"QuestPDF Basics\" on {DateTime.Now:dd MMM yyyy}.") .FontSize(20).Light(); }); }); }) .GenerateImages(x => $"page-background.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/PlaceholderExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class PlaceholderExamples { [Test] public void TextExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(15); AddItem("Name", Placeholders.Name()); AddItem("Email", Placeholders.Email()); AddItem("Phone", Placeholders.PhoneNumber()); AddItem("Date", Placeholders.ShortDate()); AddItem("Time", Placeholders.Time()); void AddItem(string label, string value) { column.Item().Text(text => { text.Span($"{label}: ").Bold(); text.Span(value); }); } }); }); }) .GenerateImages(x => "placeholders-text.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void BackgroundColorExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(320, 0)); page.MaxSize(new PageSize(320, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Grid(grid => { grid.Columns(5); grid.Spacing(5); foreach (var _ in Enumerable.Range(0, 25)) { grid.Item() .Height(50) .Width(50) .Background(Placeholders.BackgroundColor()); } }); }); }) .GenerateImages(x => "placeholders-color-background.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void ColorExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); foreach (var i in Enumerable.Range(0, 5)) { column.Item() .Text(Placeholders.Sentence()) .FontColor(Placeholders.Color()); } }); }); }) .GenerateImages(x => "placeholders-color.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void ImageExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Width(200) .Column(column => { column.Spacing(10); // provide an exact image resolution column.Item() .Image(Placeholders.Image(100, 50)); // specify physical width and height of the image column.Item() .Width(200) .Height(150) .Image(Placeholders.Image); // specify target physical width and aspect ratio column.Item() .Width(200) .AspectRatio(3 / 2f) .Image(Placeholders.Image); }); }); }) .GenerateImages(x => "placeholders-image.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void ElementExample() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Header() .Height(100) .Placeholder("Header"); page.Content() .PaddingVertical(25) .Placeholder(); page.Footer() .Height(100) .Placeholder("Footer"); }); }) .GenerateImages(x => "placeholder-element.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/PreventPageBreakExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.DocumentationExamples; public class PreventPageBreakExamples { [Test] public void EnabledExample() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(30); page.Content() .Column(column => { column.Item().Height(400).Background(Colors.Grey.Lighten3); column.Item().Height(30); column.Item() .PreventPageBreak() .Text(text => { text.ParagraphSpacing(15); text.Span("Optimizing Content Placement").Bold().FontColor(Colors.Blue.Darken2).FontSize(24); text.Span("\n"); text.Span("By carefully determining where to place a page break, you can avoid awkward text separations and maintain readability. Thoughtful formatting improves the overall user experience, making complex topics easier to digest."); }); }); }); }) .GeneratePdf("prevent-page-break-enabled.pdf"); } [Test] public void DisabledExample() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(30); page.Content() .Column(column => { column.Item().Height(400).Background(Colors.Grey.Lighten3); column.Item().Height(30); column.Item() .Text(text => { text.ParagraphSpacing(15); text.Span("Optimizing Content Placement").Bold().FontColor(Colors.Blue.Darken2).FontSize(24); text.Span("\n"); text.Span("By carefully determining where to place a page break, you can avoid awkward text separations and maintain readability. Thoughtful formatting improves the overall user experience, making complex topics easier to digest."); }); }); }); }) .GeneratePdf("prevent-page-break-disabled.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/QuestPDF.DocumentationExamples.csproj ================================================ net10.0 enable enable en false true all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive PreserveNewest ================================================ FILE: Source/QuestPDF.DocumentationExamples/RepeatExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class RepeatExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(600, 0)); page.MaxSize(new PageSize(600, 600)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Decoration(decoration => { var terms = new[] { ("Algorithm", "A precise set of instructions that defines a process for solving a specific problem or performing a computation. Algorithms are the foundation of programming and are used to optimize tasks efficiently."), ("Bug", "An error, flaw, or unintended behavior in a program that causes it to produce incorrect or unexpected results. Debugging is the process of identifying, analyzing, and fixing these issues to improve software reliability."), ("Variable", "A named storage location in memory that holds a value, which can be modified during program execution. Variables make code dynamic and flexible by allowing data manipulation and retrieval."), ("Compilation", "The process of transforming human-readable source code into machine code (binary instructions) that a computer can execute. This process is performed by a compiler and often includes syntax checks, optimizations, and linking dependencies.") }; decoration.Before().Text("Terms and their definitions:").Bold(); decoration.Content().PaddingTop(15).Column(column => { foreach (var term in terms) { column.Item().Row(row => { row.RelativeItem(2) .Border(1) .Background(Colors.Grey.Lighten3) .Padding(15) .Repeat() .Text(term.Item1); row.RelativeItem(3) .Border(1) .Padding(15) .Text(term.Item2); }); } }); }); }); }) .GenerateImages(x => $"repeat-with-{x}.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/Resources/semantic-book-content.json ================================================ [ { "term": "Variable", "description": "A storage location paired with an associated symbolic name, which contains some known or unknown quantity of information referred to as a value.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Primitives" }, { "term": "Function", "description": "A block of reusable code that performs a specific task and can be called by name.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Control Flow", "thirdLevelCategory": "Subroutines" }, { "term": "If-Else Statement", "description": "A conditional statement that executes a block of code if a specified condition is true, and another block if it is false.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Control Flow", "thirdLevelCategory": "Conditionals" }, { "term": "For Loop", "description": "A control flow statement for specifying iteration, which allows code to be executed repeatedly.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Control Flow", "thirdLevelCategory": "Loops" }, { "term": "Integer", "description": "A data type that represents a whole number, without a fractional component.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Primitives" }, { "term": "String", "description": "A data type representing a sequence of characters.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Primitives" }, { "term": "Boolean", "description": "A data type with only two possible values: true or false.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Primitives" }, { "term": "Array", "description": "A data structure consisting of a collection of elements, each identified by at least one array index or key.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Data Structures", "thirdLevelCategory": "Linear" }, { "term": "Object", "description": "A data structure that contains data in the form of key-value pairs.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Composite" }, { "term": "Null", "description": "A special value representing the intentional absence of any object value.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Primitives" }, { "term": "Class", "description": "A blueprint for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions).", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Object-Oriented", "thirdLevelCategory": "Core Constructs" }, { "term": "Inheritance", "description": "A mechanism where a new class derives properties and behavior from an existing class.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Object-Oriented", "thirdLevelCategory": "Principles" }, { "term": "Polymorphism", "description": "The provision of a single interface to entities of different types, allowing objects to be treated as instances of their parent class.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Object-Oriented", "thirdLevelCategory": "Principles" }, { "term": "Encapsulation", "description": "The bundling of data with the methods that operate on that data, or the restricting of direct access to some of an object's components.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Object-Oriented", "thirdLevelCategory": "Principles" }, { "term": "Abstraction", "description": "The concept of hiding the complex reality while exposing only the necessary parts.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Object-Oriented", "thirdLevelCategory": "Principles" }, { "term": "Algorithm", "description": "A finite sequence of well-defined, computer-implementable instructions to solve a class of problems.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Algorithms", "thirdLevelCategory": "Fundamentals" }, { "term": "Linked List", "description": "A linear collection of data elements whose order is not given by their physical placement in memory. Each element points to the next.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Data Structures", "thirdLevelCategory": "Linear" }, { "term": "Stack", "description": "A linear data structure that follows the Last-In, First-Out (LIFO) principle.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Data Structures", "thirdLevelCategory": "Linear" }, { "term": "Queue", "description": "A linear data structure that follows the First-In, First-Out (FIFO) principle.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Data Structures", "thirdLevelCategory": "Linear" }, { "term": "Tree", "description": "A hierarchical data structure with a root value and subtrees of children with a parent node, represented as a set of linked nodes.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Data Structures", "thirdLevelCategory": "Non-Linear" }, { "term": "Graph", "description": "A data structure consisting of a set of vertices (or nodes) and a set of edges that connect these vertices.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Data Structures", "thirdLevelCategory": "Non-Linear" }, { "term": "Hash Table", "description": "A data structure that implements an associative array abstract data type, a structure that can map keys to values.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Data Structures", "thirdLevelCategory": "Key-Value" }, { "term": "Binary Search", "description": "A search algorithm that finds the position of a target value within a sorted array.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Algorithms", "thirdLevelCategory": "Searching" }, { "term": "Bubble Sort", "description": "A simple sorting algorithm that repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Algorithms", "thirdLevelCategory": "Sorting" }, { "term": "Merge Sort", "description": "An efficient, comparison-based, divide and conquer sorting algorithm.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Algorithms", "thirdLevelCategory": "Sorting" }, { "term": "Big O Notation", "description": "A mathematical notation that describes the limiting behavior of a function when the argument tends towards a particular value or infinity, used to classify algorithms according to their running time or space requirements.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Complexity", "thirdLevelCategory": "Asymptotic Analysis" }, { "term": "Recursion", "description": "A method of solving a problem where the solution depends on solutions to smaller instances of the same problem.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Control Flow", "thirdLevelCategory": "Subroutines" }, { "term": "API (Application Programming Interface)", "description": "A set of rules and protocols for building and interacting with software applications.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Architecture", "thirdLevelCategory": "Integration" }, { "term": "REST (Representational State Transfer)", "description": "An architectural style for designing networked applications, often used for creating web services.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Architecture", "thirdLevelCategory": "Integration" }, { "term": "HTTP (Hypertext Transfer Protocol)", "description": "The foundation of data communication for the World Wide Web.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Protocols", "thirdLevelCategory": "Communication" }, { "term": "JSON (JavaScript Object Notation)", "description": "A lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse and generate.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Data Formats", "thirdLevelCategory": "Serialization" }, { "term": "HTML (Hypertext Markup Language)", "description": "The standard markup language for documents designed to be displayed in a web browser.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Frontend", "thirdLevelCategory": "Markup" }, { "term": "CSS (Cascading Style Sheets)", "description": "A style sheet language used for describing the presentation of a document written in a markup language like HTML.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Frontend", "thirdLevelCategory": "Styling" }, { "term": "JavaScript", "description": "A high-level, interpreted programming language that conforms to the ECMAScript specification, primarily used for web development.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Frontend", "thirdLevelCategory": "Scripting" }, { "term": "DOM (Document Object Model)", "description": "A programming interface for HTML and XML documents. It represents the page so that programs can change the document structure, style, and content.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Frontend", "thirdLevelCategory": "APIs" }, { "term": "Frontend", "description": "The part of a website or application that the user interacts with directly; also known as client-side.", "firstLevelCategory": "Web Development", "secondLevelCategory": "General Concepts", "thirdLevelCategory": "Client-Side" }, { "term": "Backend", "description": "The server-side of a website or application, responsible for storing and organizing data and ensuring everything on the client-side works.", "firstLevelCategory": "Web Development", "secondLevelCategory": "General Concepts", "thirdLevelCategory": "Server-Side" }, { "term": "Database", "description": "An organized collection of data, generally stored and accessed electronically from a computer system.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Data Storage", "thirdLevelCategory": "Systems" }, { "term": "SQL (Structured Query Language)", "description": "A domain-specific language used in programming and designed for managing data held in a relational database management system.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Data Storage", "thirdLevelCategory": "Query Languages" }, { "term": "NoSQL", "description": "A database that provides a mechanism for storage and retrieval of data that is modeled in means other than the tabular relations used in relational databases.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Data Storage", "thirdLevelCategory": "Systems" }, { "term": "Git", "description": "A distributed version-control system for tracking changes in source code during software development.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Version Control", "thirdLevelCategory": "Systems" }, { "term": "Commit", "description": "An operation in version control which saves the current state of changes to the local repository.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Version Control", "thirdLevelCategory": "Operations" }, { "term": "Branch", "description": "A parallel version of a repository in version control, allowing for independent development without affecting the main line.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Version Control", "thirdLevelCategory": "Concepts" }, { "term": "Merge", "description": "An operation in version control that integrates changes from different branches into a single branch.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Version Control", "thirdLevelCategory": "Operations" }, { "term": "Repository", "description": "A central location in which data is stored and managed, commonly used for source code.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Version Control", "thirdLevelCategory": "Concepts" }, { "term": "Compiler", "description": "A special program that processes statements written in a particular programming language and turns them into machine language or 'code' that a computer's processor uses.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Language Processors", "thirdLevelCategory": "Translation" }, { "term": "Interpreter", "description": "A computer program that directly executes instructions written in a programming or scripting language, without requiring them to have been previously compiled into a machine language program.", "firstLevelCategory": "Tools &Technologies", "secondLevelCategory": "Language Processors", "thirdLevelCategory": "Execution" }, { "term": "IDE (Integrated Development Environment)", "description": "A software application that provides comprehensive facilities to computer programmers for software development.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Development Tools", "thirdLevelCategory": "Editors" }, { "term": "Debugger", "description": "A computer program used to test and find bugs (errors) in other programs.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Development Tools", "thirdLevelCategory": "Testing" }, { "term": "Library", "description": "A collection of non-volatile resources used by computer programs, often for software development.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Code Reusability", "thirdLevelCategory": "Collections" }, { "term": "Framework", "description": "A pre-written, structured body of code that provides a standard way to build and deploy applications.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Code Reusability", "thirdLevelCategory": "Scaffolding" }, { "term": "Agile", "description": "A set of practices for software development under which requirements and solutions evolve through the collaborative effort of self-organizing cross-functional teams.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Methodologies", "thirdLevelCategory": "Iterative" }, { "term": "Scrum", "description": "An agile framework for managing knowledge work, with an emphasis on software development.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Methodologies", "thirdLevelCategory": "Frameworks" }, { "term": "Waterfall Model", "description": "A sequential design process in which progress is seen as flowing steadily downwards (like a waterfall) through the phases of conception, initiation, analysis, design, construction, testing, deployment and maintenance.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Methodologies", "thirdLevelCategory": "Sequential" }, { "term": "Unit Testing", "description": "A level of software testing where individual units or components of a software are tested.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Testing", "thirdLevelCategory": "Levels" }, { "term": "Integration Testing", "description": "A level of software testing where individual units are combined and tested as a group.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Testing", "thirdLevelCategory": "Levels" }, { "term": "CI/CD (Continuous Integration/Continuous Deployment)", "description": "The combined practices of continuous integration and either continuous delivery or continuous deployment, aimed at frequent and reliable software releases.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "DevOps", "thirdLevelCategory": "Automation" }, { "term": "DevOps", "description": "A set of practices that combines software development (Dev) and IT operations (Ops) to shorten the systems development life cycle and provide continuous delivery with high software quality.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "DevOps", "thirdLevelCategory": "Culture" }, { "term": "Syntax", "description": "The set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a language.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Language Fundamentals", "thirdLevelCategory": "Grammar" }, { "term": "Semantics", "description": "The meaning of a programming language's statements.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Language Fundamentals", "thirdLevelCategory": "Meaning" }, { "term": "Functional Programming", "description": "A programming paradigm where programs are constructed by applying and composing functions.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Functional", "thirdLevelCategory": "Core Concepts" }, { "term": "Pure Function", "description": "A function whose return value is only determined by its input values, without observable side effects.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Functional", "thirdLevelCategory": "Principles" }, { "term": "Immutability", "description": "A principle where an object's state cannot be modified after it is created.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Functional", "thirdLevelCategory": "Principles" }, { "term": "Higher-Order Function", "description": "A function that either takes one or more functions as arguments or returns a function as its result.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Functional", "thirdLevelCategory": "Core Concepts" }, { "term": "Asynchronous", "description": "Operations that allow a program to start a potentially long-running task and still be able to be responsive to other events while that task runs.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Execution Model", "thirdLevelCategory": "Concurrency" }, { "term": "Promise", "description": "An object representing the eventual completion or failure of an asynchronous operation.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Execution Model", "thirdLevelCategory": "Concurrency" }, { "term": "Thread", "description": "The smallest sequence of programmed instructions that can be managed independently by a scheduler.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Execution Model", "thirdLevelCategory": "Parallelism" }, { "term": "Garbage Collection", "description": "A form of automatic memory management that attempts to reclaim memory occupied by objects that are no longer in use by the program.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Memory Management", "thirdLevelCategory": "Automatic" }, { "term": "Pointer", "description": "A variable whose value is the memory address of another variable.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Memory Management", "thirdLevelCategory": "Manual" }, { "term": "SDK (Software Development Kit)", "description": "A collection of software development tools in one installable package.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Development Tools", "thirdLevelCategory": "Tooling" }, { "term": "Node.js", "description": "A JavaScript runtime built on Chrome's V8 JavaScript engine, used for building server-side applications.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Backend", "thirdLevelCategory": "Runtimes" }, { "term": "React", "description": "A JavaScript library for building user interfaces, particularly for single-page applications.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Frontend", "thirdLevelCategory": "Libraries" }, { "term": "Angular", "description": "A platform and framework for building single-page client applications using HTML and TypeScript.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Frontend", "thirdLevelCategory": "Frameworks" }, { "term": "Vue.js", "description": "A progressive framework for building user interfaces.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Frontend", "thirdLevelCategory": "Frameworks" }, { "term": "Docker", "description": "A set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Deployment", "thirdLevelCategory": "Containerization" }, { "term": "Kubernetes", "description": "An open-source container-orchestration system for automating computer application deployment, scaling, and management.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Deployment", "thirdLevelCategory": "Orchestration" }, { "term": "Microservices", "description": "An architectural style that structures an application as a collection of loosely coupled services.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Architecture", "thirdLevelCategory": "Design Patterns" }, { "term": "Monolithic Architecture", "description": "An architectural style where an application is built as a single, indivisible unit.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Architecture", "thirdLevelCategory": "Design Patterns" }, { "term": "Cookie", "description": "A small piece of data sent from a website and stored on the user's computer by the user's web browser while the user is browsing.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Protocols", "thirdLevelCategory": "State Management" }, { "term": "Session", "description": "A way to store information (in variables) to be used across multiple pages on a server.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Protocols", "thirdLevelCategory": "State Management" }, { "term": "Cache", "description": "A hardware or software component that stores data so that future requests for that data can be served faster.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Performance", "thirdLevelCategory": "Optimization" }, { "term": "Time Complexity", "description": "The computational complexity that describes the amount of computer time it takes to run an algorithm.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Complexity", "thirdLevelCategory": "Performance" }, { "term": "Space Complexity", "description": "The amount of memory space required to solve an instance of the computational problem as a function of the input size.", "firstLevelCategory": "Data Structures & Algorithms", "secondLevelCategory": "Complexity", "thirdLevelCategory": "Memory" }, { "term": "Procedural Programming", "description": "A programming paradigm based upon the concept of procedure calls, where statements are structured into procedures (or subroutines).", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Procedural", "thirdLevelCategory": "Core Concepts" }, { "term": "Operator", "description": "A symbol that tells the compiler or interpreter to perform specific mathematical, relational, or logical operations.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Language Fundamentals", "thirdLevelCategory": "Expressions" }, { "term": "Expression", "description": "A combination of one or more constants, variables, operators, and functions that the programming language interprets and computes to produce another value.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Language Fundamentals", "thirdLevelCategory": "Expressions" }, { "term": "Deployment", "description": "The process of making a software system available for use.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Deployment", "thirdLevelCategory": "Process" }, { "term": "Middleware", "description": "Software that lies between an operating system and the applications running on it, enabling communication and data management.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Backend", "thirdLevelCategory": "Architecture" }, { "term": "Endpoint", "description": "One end of a communication channel. When an API interacts with another system, the touchpoints of this communication are considered endpoints.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Architecture", "thirdLevelCategory": "Integration" }, { "term": "Type System", "description": "A set of rules that assigns a property called type to the various constructs of a computer program.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Typing" }, { "term": "Static Typing", "description": "Type checking is performed during compile-time.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Typing" }, { "term": "Dynamic Typing", "description": "Type checking is performed at run-time.", "firstLevelCategory": "Core Concepts", "secondLevelCategory": "Data Types", "thirdLevelCategory": "Typing" }, { "term": "Lambda Function", "description": "An anonymous function that can be defined without being bound to an identifier.", "firstLevelCategory": "Programming Paradigms", "secondLevelCategory": "Functional", "thirdLevelCategory": "Core Concepts" }, { "term": "Environment Variable", "description": "A variable whose value is set outside the program, typically through functionality built into the operating system or microservice.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Configuration", "thirdLevelCategory": "Runtime" }, { "term": "CLI (Command-Line Interface)", "description": "A text-based user interface used to view and manage computer files.", "firstLevelCategory": "Tools & Technologies", "secondLevelCategory": "Interfaces", "thirdLevelCategory": "Text-Based" }, { "term": "HTTPS", "description": "An extension of the Hypertext Transfer Protocol for secure communication over a computer network.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Protocols", "thirdLevelCategory": "Security" }, { "term": "Authentication", "description": "The process of verifying the identity of a user or process.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Security", "thirdLevelCategory": "Access Control" }, { "term": "Authorization", "description": "The process of specifying access rights/privileges to resources.", "firstLevelCategory": "Web Development", "secondLevelCategory": "Security", "thirdLevelCategory": "Access Control" }, { "term": "Regression Testing", "description": "A type of software testing to confirm that a recent program or code change has not adversely affected existing features.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Testing", "thirdLevelCategory": "Types" }, { "term": "Code Review", "description": "A software quality assurance activity in which one or several humans check a program mainly by viewing and reading parts of its source code.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Quality Assurance", "thirdLevelCategory": "Practices" }, { "term": "Refactoring", "description": "The process of restructuring existing computer code—changing the factoring—without changing its external behavior.", "firstLevelCategory": "Software Development Lifecycle", "secondLevelCategory": "Maintenance", "thirdLevelCategory": "Code Improvement" } ] ================================================ FILE: Source/QuestPDF.DocumentationExamples/RotateExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class RotateExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Row(row => { row.AutoItem() .RotateLeft() .AlignCenter() .Text("Definition") .Bold().FontColor(Colors.Blue.Darken2); row.AutoItem() .PaddingHorizontal(15) .LineVertical(2).LineColor(Colors.Blue.Medium); row.RelativeItem() .Background(Colors.Blue.Lighten5) .Padding(15) .Text(text => { text.Span("A variable").Bold(); text.Span(" is a named storage location in memory that holds a value which can be modified during program execution."); }); }); }); }) .GenerateImages(x => "rotate.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void FreeExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.Content() .Background(Colors.Grey.Lighten2) .Padding(25) .Row(row => { row.Spacing(25); AddIcon(0); AddIcon(30); AddIcon(45); AddIcon(80); void AddIcon(float angle) { const float itemSize = 100; row.AutoItem() .Width(itemSize) .AspectRatio(1) .TranslateX(itemSize / 2) .TranslateY(itemSize / 2) .Rotate(angle) .TranslateX(-itemSize / 2) .TranslateY(-itemSize / 2) .Svg("Resources/compass.svg"); } }); }); }) .GenerateImages(x => "rotate-free.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/RoundedCornersExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class RoundedCornersExamples { [Test] public void Consistent() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Border(1, Colors.Black) .Background(Colors.Grey.Lighten3) .CornerRadius(25) .Padding(25) .Text("Container with consistently rounded corners"); }); }) .GenerateImages(x => "rounded-corners-consistent.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Various() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Border(1, Colors.Black) .Background(Colors.Grey.Lighten3) .CornerRadiusTopLeft(5) .CornerRadiusTopRight(10) .CornerRadiusBottomRight(20) .CornerRadiusBottomLeft(40) .Padding(25) .Text("Container with rounded corners"); }); }) .GenerateImages(x => "rounded-corners-various.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Complex() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(550, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Border(1, Colors.Black) .CornerRadius(15) .Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(100); columns.RelativeColumn(); columns.ConstantColumn(150); }); table.Header(header => { header.Cell().Element(Style).Text("Index"); header.Cell().Element(Style).Text("Label"); header.Cell().Element(Style).Text("Price"); IContainer Style(IContainer container) { return container .Border(1, Colors.Grey.Darken2) .Background(Colors.Grey.Lighten3) .PaddingVertical(10) .PaddingHorizontal(15) .DefaultTextStyle(x => x.Bold()); } }); foreach (var index in Enumerable.Range(1, 5)) { table.Cell().Element(Style).Text(index.ToString()); table.Cell().Element(Style).Text(Placeholders.Label()); table.Cell().Element(Style).Text(Placeholders.Price()); IContainer Style(IContainer container) { return container .Border(1, Colors.Grey.Darken2) .PaddingVertical(10) .PaddingHorizontal(15); } } }); }); }) .GenerateImages(x => "rounded-corners-complex.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Image() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(450, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .CornerRadius(25) .Image("Resources/landscape.jpg"); }); }) .GenerateImages(x => "rounded-corners-image.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/RowExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class RowExamples { [Test] public void SimpleExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.Margin(25); page.Content() .Padding(25) .Width(325) .Row(row => { row.ConstantItem(100) .Background(Colors.Grey.Medium) .Padding(10) .Text("100pt"); row.RelativeItem() .Background(Colors.Grey.Lighten1) .Padding(10) .Text("75pt"); row.RelativeItem(2) .Background(Colors.Grey.Lighten2) .Padding(10) .Text("150pt"); }); }); }) .GenerateImages(x => "row-simple.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void SpacingExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.Margin(25); page.Content() .Padding(25) .Width(220) .Height(50) .Row(row => { row.Spacing(10); row.RelativeItem(2).Background(Colors.Grey.Medium); row.RelativeItem(3).Background(Colors.Grey.Lighten1); row.RelativeItem(5).Background(Colors.Grey.Lighten2); }); }); }) .GenerateImages(x => "row-spacing.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void CustomSpacingExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(250, 0)); page.MaxSize(new PageSize(250, 1000)); page.Margin(25); page.Content() .Height(50) .Row(row => { row.RelativeItem().Background(Colors.Grey.Darken1); row.ConstantItem(10); row.RelativeItem().Background(Colors.Grey.Medium); row.ConstantItem(20); row.RelativeItem().Background(Colors.Grey.Lighten1); row.ConstantItem(30); row.RelativeItem().Background(Colors.Grey.Lighten2); }); }); }) .GenerateImages(x => "row-spacing-custom.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void DisableUniformItemsHeightExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(700, 0)); page.MaxSize(new PageSize(700, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(15); row.RelativeItem() .Element(LabelStyle) .Text("Programming is both a science and an art — it demands precision, creativity, and patience. At its core, it’s about understanding how to break down complex problems into small, logical steps that a computer can execute."); row.RelativeItem() .Element(LabelStyle) .Text("Programming is the art of turning ideas into logic, logic into code, and code into something that solves real problems."); static IContainer LabelStyle(IContainer container) => container .ShrinkVertical() .Background(Colors.Grey.Lighten3) .CornerRadius(15) .Padding(15); }); }); }) .GenerateImages(x => "row-uniform-height-enabled.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ScaleExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ScaleExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(350) .Padding(25) .Column(column => { column.Spacing(10); var scales = new[] { 0.75f, 1f, 1.25f, 1.5f }; foreach (var scale in scales) { column .Item() .Background(Colors.Grey.Lighten3) .Scale(scale) .Padding(10) .Text($"Content scale: {scale}") .FontSize(20); } }); }); }) .GenerateImages(x => "scale.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ScaleToFitExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ScaleToFitExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."; foreach (var i in Enumerable.Range(4, 5)) { column .Item() .Shrink() .Border(1) .Padding(15) .Width(i * 50) // sizes from 200x100 to 450x175 .Height(i * 25) .ScaleToFit() .Text(text); } }); }); }) .GenerateImages(x => "scale-to-fit.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/SectionExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class SectionExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5.Landscape()); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { var terms = new[] { ("Bit", "The smallest unit of data in computing, representing either a 0 or a 1. Multiple bits are combined to form bytes, which are used to store larger data values."), ("Byte", "A unit of digital information that consists of 8 bits. A byte is commonly used to store a single character of text, such as a letter or a number, in computer memory."), ("Binary", "A number system that uses only two digits, 0 and 1, which are the fundamental building blocks of computer operations. Computers process and store all data in binary format, including text, images, and instructions."), ("Array", "A data structure that stores a fixed-size sequence of elements, all of the same type, in a contiguous block of memory. Arrays allow quick access to elements using an index and are commonly used to manage collections of data.") }; // title column.Item().Extend().AlignMiddle().AlignCenter().Text("Programming Glossary").FontSize(32).Bold(); column.Item().PageBreak(); // table of contents column.Item().PaddingBottom(25).Text("Table of Contents").FontSize(24).Bold().Underline(); foreach (var term in terms) { column.Item() .PaddingBottom(10) .SectionLink($"term-{term}") .Text(text => { text.Span("Term "); text.Span(term.Item1).Bold(); text.Span(" on page "); text.BeginPageNumberOfSection($"term-{term}"); }); } // content foreach (var term in terms) { column.Item().PageBreak(); column.Item() .Section($"term-{term}") .Text(text => { text.Span(term.Item1).Bold().FontColor(Colors.Blue.Darken2); text.Span(" - "); text.Span(term.Item2); }); } }); }); }) .GeneratePdf("sections.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/SemanticExamples.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class SemanticExamples { [Test] public void HeaderAndFooter() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 250)); page.DefaultTextStyle(x => x.FontSize(16)); page.Margin(25); page.Content() .Border(1) .BorderColor(Colors.Grey.Lighten1) .SemanticTable() .Table(table => { var pageSizes = new List<(string name, double width, double height)>() { ("Letter (ANSI A)", 8.5f, 11), ("Legal", 8.5f, 14), ("Ledger (ANSI B)", 11, 17), ("Tabloid (ANSI B)", 17, 11), ("ANSI C", 22, 17), ("ANSI D", 34, 22), ("ANSI E", 44, 34) }; const int inchesToPoints = 72; IContainer DefaultCellStyle(IContainer container, string backgroundColor) { return container .Border(1) .BorderColor(Colors.Grey.Lighten1) .Background(backgroundColor) .PaddingVertical(5) .PaddingHorizontal(10) .AlignCenter() .AlignMiddle(); } table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.ConstantColumn(80); columns.ConstantColumn(80); columns.ConstantColumn(80); columns.ConstantColumn(80); }); table.Header(header => { // please be sure to call the 'header' handler! header.Cell().RowSpan(2).Element(CellStyle).ExtendHorizontal().AlignLeft() .Text("Document type").Bold(); header.Cell().ColumnSpan(2).Element(CellStyle).Text("Inches").Bold(); header.Cell().ColumnSpan(2).Element(CellStyle).Text("Points").Bold(); header.Cell().Element(CellStyle).Text("Width"); header.Cell().Element(CellStyle).Text("Height"); header.Cell().Element(CellStyle).Text("Width"); header.Cell().Element(CellStyle).Text("Height"); // you can extend existing styles by creating additional methods IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.Grey.Lighten3); }); foreach (var page in pageSizes) { table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().Text(page.name); // inches table.Cell().Element(CellStyle).Text(page.width); table.Cell().Element(CellStyle).Text(page.height); // points table.Cell().Element(CellStyle).Text(page.width * inchesToPoints); table.Cell().Element(CellStyle).Text(page.height * inchesToPoints); IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.White).ShowOnce(); } }); }); }) .GeneratePdf(); } public class BookTermModel { public string Term { get; set; } public string Description { get; set; } public string FirstLevelCategory { get; set; } public string SecondLevelCategory { get; set; } public string ThirdLevelCategory { get; set; } } [Test] public async Task GenerateBook() { QuestPDF.Settings.EnableCaching = false; QuestPDF.Settings.EnableDebugging = false; var serializerSettings = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true }; var bookData = await File.ReadAllTextAsync("Resources/semantic-book-content.json"); var terms = JsonSerializer.Deserialize>(bookData, serializerSettings); var categories = terms .GroupBy(x => x.FirstLevelCategory) .Select(x => new { Category = x.Key, Terms = x .GroupBy(y => y.SecondLevelCategory) .Select(y => new { Category = y.Key, Terms = y .GroupBy(z => z.ThirdLevelCategory) .Select(z => new { Category = z.Key, Terms = z.ToList() }) .ToList() }) .ToList() }) .ToList(); Document .Create(document => { document.Page(page => { page.Size(PageSizes.A4); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Header() .Text("Programming Terms") .Bold() .FontSize(36); page.Content() .PaddingVertical(24) .Column(column => { foreach (var category1 in categories) { column.Item() .SemanticSection() .EnsureSpace(100) .Column(column => { column.Spacing(24); column.Item() .PaddingBottom(8) .SemanticHeader1() .Text(category1.Category) .FontSize(24) .FontColor(Colors.Blue.Darken4) .Bold(); foreach (var category2 in category1.Terms) { column.Item() .SemanticSection() .EnsureSpace(100) .Column(column => { column.Spacing(8); column.Item() .PaddingBottom(8) .SemanticHeader2() .Text(category2.Category) .FontSize(20) .FontColor(Colors.Blue.Darken2) .Bold(); foreach (var category3 in category2.Terms) { column.Item() .SemanticSection() .EnsureSpace(100) .Column(column => { column.Spacing(8); column.Item() .PaddingBottom(8) .SemanticHeader3() .Text(category3.Category) .FontSize(16) .FontColor(Colors.Blue.Medium) .Bold(); foreach (var term in category3.Terms) { column.Item() .SemanticParagraph() .Text(text => { text.Span(term.Term).Bold(); text.Span(" - "); text.Span(term.Description); }); } }); } }); } }); column.Item().PageBreak(); } }); page.Footer() .AlignCenter() .Text(text => { text.Span("Page "); text.CurrentPageNumber(); text.Span(" of "); text.TotalPages(); }); }); }) .WithMetadata(new DocumentMetadata() { Title = "Programming Terms", Language = "en-US" }) .GeneratePdf(); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ShadowExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ShadowExamples { [Test] public void Simple() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Content() .Border(1, Colors.Black) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Medium, Blur = 5, Spread = 5, OffsetX = 5, OffsetY = 5 }) .Background(Colors.White) .Padding(15) .Text("Important content"); }); }) .GenerateImages(x => $"shadow-simple.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void OffsetX() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(50); foreach (var offsetX in new[] { -10, 0, 10 }) { row.ConstantItem(100) .AspectRatio(1) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken1, Blur = 10, OffsetX = offsetX }) .Background(Colors.White); } }); }); }) .GenerateImages(x => $"shadow-offset-x.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void OffsetY() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(50); foreach (var offsetY in new[] { -10, 0, 10 }) { row.ConstantItem(100) .AspectRatio(1) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken2, Blur = 10, OffsetY = offsetY }) .Background(Colors.White); } }); }); }) .GenerateImages(x => $"shadow-offset-y.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Color() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(50); var colors = new[] { Colors.Red.Darken2, Colors.Green.Darken2, Colors.Blue.Darken2 }; foreach (var color in colors) { row.ConstantItem(100) .AspectRatio(1) .Shadow(new BoxShadowStyle { Color = color, Blur = 10 }) .Background(Colors.White); } }); }); }) .GenerateImages(x => $"shadow-color.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Blur() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(50); foreach (var blur in new[] { 5, 10, 20 }) { row.ConstantItem(100) .AspectRatio(1) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken1, Blur = blur }) .Background(Colors.White); } }); }); }) .GenerateImages(x => $"shadow-blur.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Spread() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(50); foreach (var spread in new[] { 0, 5, 10 }) { row.ConstantItem(100) .AspectRatio(1) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken1, Blur = 5, Spread = spread }) .Background(Colors.White); } }); }); }) .GenerateImages(x => $"shadow-spread.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void NoBlur() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(50); page.PageColor(Colors.White); page.Content() .Row(row => { row.Spacing(50); row.ConstantItem(100) .AspectRatio(1) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Lighten1, Blur = 0, OffsetX = 8, OffsetY = 8 }) .Border(1, Colors.Black) .Background(Colors.White); row.ConstantItem(100) .AspectRatio(1) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Lighten1, Blur = 0, OffsetX = 8, OffsetY = 8 }) .Border(1, Colors.Black) .CornerRadius(16) .Background(Colors.White); }); }); }) .GenerateImages(x => $"shadow-no-blur.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ShowEntireExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ShowEntireExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.Size(500, 500); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Decoration(decoration => { var terms = new[] { ("Function", "A reusable block of code designed to perform a specific task. Functions take input parameters, process them, and return results, making code modular, readable, and maintainable. They are an essential component of all programming languages."), ("Recursion", "A programming technique where a function calls itself in order to solve a problem by breaking it down into smaller, similar subproblems. Recursion is often used for complex algorithms, such as searching, sorting, and tree traversal."), ("Framework", "A pre-built collection of code, tools, and best practices that provides a structured foundation for developing software. Frameworks simplify development by handling common functionalities, such as database access, user authentication, and UI rendering."), ("Package", "A self-contained collection of code, typically consisting of functions, classes, and modules, that provides specific functionality. Packages help organize large projects and allow developers to reuse and distribute their code easily."), }; decoration.Before().Text("Terms and their definitions:").FontSize(24).Bold().Underline(); decoration.Content().PaddingTop(15).Column(column => { column.Spacing(15); foreach (var term in terms) { column.Item() .ShowEntire() .Text(text => { text.Span(term.Item1).Bold().FontColor(Colors.Blue.Darken2); text.Span($" - {term.Item2}"); }); } }); }); }); }) .GenerateImages(x => $"show-entire-with-{x}.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ShowOnceExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ShowOnceExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.Size(350, 500); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Decoration(decoration => { decoration.Before().Column(column => { column.Item() .ShowOnce() .Row(row => { row.ConstantItem(80).AspectRatio(4 / 3f).Placeholder(); row.ConstantItem(10); row.RelativeItem() .AlignMiddle() .Column(innerColumn => { innerColumn.Item().Text("Invoice #1234").FontSize(24).Bold(); innerColumn.Item().Text($"Generated on {DateTime.Now:d}").FontSize(16).Light(); }); }); column.Item() .SkipOnce() .Text("Invoice #1234").FontSize(24).Bold(); }); // generate dummy content decoration.Content() .PaddingTop(15) .ExtendHorizontal() .Column(column => { column.Spacing(10); foreach (var i in Enumerable.Range(1, 15)) { column.Item() .Height(30) .Background(Colors.Grey.Lighten3) .AlignCenter() .AlignMiddle() .Text($"{i}"); } }); }); }); }) .GenerateImages(x => $"show-once-{x}.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/SkiaSharpHelpers.cs ================================================ using System.Text; using QuestPDF.Fluent; using QuestPDF.Infrastructure; using SkiaSharp; namespace QuestPDF.SkiaSharpIntegration; public static class SkiaSharpHelpers { public static void SkiaSharpSvgCanvas(this IContainer container, Action drawOnCanvas) { container.Svg(size => { using var stream = new MemoryStream(); using (var canvas = SKSvgCanvas.Create(new SKRect(0, 0, size.Width, size.Height), stream)) drawOnCanvas(canvas, size); var svgData = stream.ToArray(); return Encoding.UTF8.GetString(svgData); }); } public static void SkiaSharpRasterizedCanvas(this IContainer container, Action drawOnCanvas) { container.Image(payload => { using var bitmap = new SKBitmap(payload.ImageSize.Width, payload.ImageSize.Height); using (var canvas = new SKCanvas(bitmap)) { canvas.Scale(payload.ImageSize.Width / payload.AvailableSpace.Width, payload.ImageSize.Height / payload.AvailableSpace.Height); drawOnCanvas(canvas, new ImageSize((int)payload.AvailableSpace.Width, (int)payload.AvailableSpace.Height)); } return bitmap.Encode(SKEncodedImageFormat.Png, 100).ToArray(); }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/SkiaSharpIntegrationExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using QuestPDF.SkiaSharpIntegration; using SkiaSharp; namespace QuestPDF.DocumentationExamples; public class SkiaSharpIntegrationExamples { [Test] public void Svg() { Document .Create(document => { document.Page(page => { page.Size(350, 350); page.Margin(25); page.Content() .Width(300) .Height(300) .SkiaSharpSvgCanvas((canvas, size) => { var centerX = size.Width / 2; var centerY = size.Height / 2; var radius = Math.Min(centerX, centerY); // draw clock face using var facePaint = new SKPaint { Color = new SKColor(Colors.Blue.Lighten4) }; canvas.DrawCircle(centerX, centerY, radius, facePaint); // draw clock ticks using var tickPaint = new SKPaint { Color = new SKColor(Colors.Blue.Darken4), StrokeWidth = 4, StrokeCap = SKStrokeCap.Round }; canvas.Save(); canvas.Translate(centerX, centerY); foreach (var i in Enumerable.Range(0, 12)) { canvas.DrawLine(new SKPoint(0, radius * 0.85f), new SKPoint(0, radius * 0.95f), tickPaint); canvas.RotateDegrees(30); } canvas.Restore(); // draw clock hands using var hourHandPaint = new SKPaint { Color = new SKColor(Colors.Blue.Darken4), StrokeWidth = 8, StrokeCap = SKStrokeCap.Round }; using var minuteHandPaint = new SKPaint { Color = new SKColor(Colors.Blue.Darken2), StrokeWidth = 4, StrokeCap = SKStrokeCap.Round }; canvas.Translate(centerX, centerY); canvas.Save(); canvas.RotateDegrees(6 * DateTime.Now.Minute); canvas.DrawLine(new SKPoint(0, 0), new SKPoint(0, -radius * 0.7f), minuteHandPaint); canvas.Restore(); canvas.Save(); canvas.RotateDegrees(30 * DateTime.Now.Hour + DateTime.Now.Minute / 2); canvas.DrawLine(new SKPoint(0, 0), new SKPoint(0, -radius * 0.5f), hourHandPaint); canvas.Restore(); }); }); }) .GenerateImages(x => "skiasharp-integration-svg.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Resterized() { Document .Create(document => { document.Page(page => { page.Size(new PageSize(500, 400)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Padding(25) .SkiaSharpRasterizedCanvas((canvas, size) => { // add padding to properly display the shadow effect const float padding = 25; canvas.Translate(padding, padding); // load image and scale canvas space using var bitmap = SKBitmap.Decode("Resources/landscape.jpg"); var targetBitmapSize = new SKSize(size.Width - 2 * padding, size.Height - 2 * padding); var scale = Math.Min(targetBitmapSize.Width / bitmap.Width, targetBitmapSize.Height / bitmap.Height); canvas.Scale(scale); var drawingArea = new SKRoundRect(new SKRect(0, 0, bitmap.Width, bitmap.Height), 32, 32); // draw drop shadow using var dropShadowFilter = SKImageFilter.CreateDropShadow(8, 8, 16, 16, SKColors.Black); using var paint = new SKPaint { ImageFilter = dropShadowFilter }; canvas.DrawRoundRect(drawingArea, paint); // draw image canvas.ClipRoundRect(drawingArea, antialias: true); canvas.DrawBitmap(bitmap, SKPoint.Empty); }); }); }) .GenerateImages(x => "skiasharp-integration-rasterized.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/SkipOnceExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class SkipOnceExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.Size(500, 500); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { var terms = new[] { ("Repository", "A centralized storage location for source code and related files, typically managed using version control systems like Git. Repositories allow multiple developers to collaborate on projects, track changes, and maintain version history."), ("Version Control", "A system that tracks changes to code over time, enabling developers to collaborate efficiently, revert to previous versions, and maintain a structured development workflow. Popular version control tools include Git, Mercurial, and Subversion."), ("Abstraction", "A programming concept that hides complex implementation details and exposes only the necessary parts. Abstraction helps simplify code and allows developers to focus on high-level design rather than low-level implementation details."), ("Namespace", "A container that groups related identifiers, such as variables, functions, and classes, to prevent naming conflicts in a program. Namespaces are commonly used in large projects to organize code efficiently."), }; column.Spacing(15); foreach (var term in terms) { column.Item().Decoration(decoration => { decoration.Before() .DefaultTextStyle(x => x.FontSize(24).Bold().FontColor(Colors.Blue.Darken2)) .Column(innerColumn => { innerColumn.Item().ShowOnce().Text(term.Item1); innerColumn.Item().SkipOnce().Text(text => { text.Span(term.Item1); text.Span(" (continued)").Light().Italic(); }); }); decoration.Content().Text(term.Item2); }); } }); }); }) .GenerateImages(x => $"skip-once-{x}.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/StopPagingExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class StopPagingExamples { [Test] public void Example() { const string bookDescription = "\"Master Modern C# Development\" is a comprehensive guide that takes you from the basics to advanced concepts in C# programming. Perfect for beginners and intermediate developers looking to enhance their skills with practical examples and real-world applications. Covering object-oriented programming, LINQ, asynchronous programming, and the latest .NET features, this book provides step-by-step explanations to help you write clean, efficient, and scalable code. Whether you're building desktop, web, or cloud applications, this resource equips you with the knowledge and best practices to become a confident C# developer."; Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Width(400) .Height(300) .StopPaging() .Decoration(decoration => { decoration.Before().Text("Book description:").Bold(); decoration.Content().Text(bookDescription); }); }); }) .GeneratePdf("stop-paging-enabled.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/TableExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class TableExamples { [Test] public void Basic() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(50); columns.RelativeColumn(); columns.ConstantColumn(125); }); table.Header(header => { header.Cell().BorderBottom(2).Padding(8).Text("#"); header.Cell().BorderBottom(2).Padding(8).Text("Product"); header.Cell().BorderBottom(2).Padding(8).AlignRight().Text("Price"); }); foreach (var i in Enumerable.Range(0, 6)) { var price = Math.Round(Random.Shared.NextDouble() * 100, 2); table.Cell().Padding(8).Text($"{i + 1}"); table.Cell().Padding(8).Text(Placeholders.Label()); table.Cell().Padding(8).AlignRight().Text($"${price}"); } }); }); }) .GenerateImages(x => "table-simple.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void CellStyleExample() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); string[] weatherIcons = ["cloudy.svg", "lightning.svg", "pouring.svg", "rainy.svg", "snowy.svg", "windy.svg"]; page.Content() .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.ConstantColumn(125); columns.ConstantColumn(125); }); table.Header(header => { header.Cell().Element(CellStyle).Text("Day"); header.Cell().Element(CellStyle).AlignCenter().Text("Weather"); header.Cell().Element(CellStyle).AlignRight().Text("Temp"); static IContainer CellStyle(IContainer container) { return container .Background(Colors.Blue.Darken2) .DefaultTextStyle(x => x.FontColor(Colors.White).Bold()) .PaddingVertical(8) .PaddingHorizontal(16); } }); foreach (var i in Enumerable.Range(0, 7)) { var weatherIndex = Random.Shared.Next(0, weatherIcons.Length); table.Cell().Element(CellStyle) .Text(new DateTime(2025, 2, 26).AddDays(i).ToString("dd MMMM")); table.Cell().Element(CellStyle).AlignCenter().Height(24) .Svg($"Resources/WeatherIcons/{weatherIcons[weatherIndex]}"); table.Cell().Element(CellStyle).AlignRight() .Text($"{Random.Shared.Next(-10, 35)}°"); IContainer CellStyle(IContainer container) { var backgroundColor = i % 2 == 0 ? Colors.Blue.Lighten5 : Colors.Blue.Lighten4; return container .Background(backgroundColor) .PaddingVertical(8) .PaddingHorizontal(16); } } }); }); }) .GenerateImages(x => "table-cell-style.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void OverlappingCells() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(700, 1000)); page.DefaultTextStyle(x => x.FontSize(16)); page.Margin(25); string[] dayNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]; page.Content() .Border(1) .BorderColor(Colors.Grey.Lighten1) .Table(table => { table.ColumnsDefinition(columns => { // hour column columns.ConstantColumn(60); // day columns foreach (var i in Enumerable.Range(0, 5)) columns.RelativeColumn(); }); // even/odd columns background foreach (var column in Enumerable.Range(0, 7)) { var backgroundColor = column % 2 == 0 ? Colors.Grey.Lighten3 : Colors.White; table.Cell().Column((uint)column).RowSpan(24).Background(backgroundColor); } // hours and hour lines foreach (var hour in Enumerable.Range(6, 10)) { table.Cell().Column(1).Row((uint)hour) .PaddingVertical(5).PaddingHorizontal(10).AlignRight() .Text($"{hour}"); table.Cell().Row((uint)hour).ColumnSpan(6) .Border(1).BorderColor(Colors.Grey.Lighten1).Height(20); } // dates and day names foreach (var i in Enumerable.Range(0, 5)) { table.Cell() .Column((uint) i + 2).Row(1).Padding(5) .Column(column => { column.Item().AlignCenter().Text($"{17 + i}").FontSize(24).Bold(); column.Item().AlignCenter().Text(dayNames[i]).Light(); }); } // standup events foreach (var i in Enumerable.Range(1, 4)) AddEvent((uint)i, 8, 1, "Standup", Colors.Blue.Lighten4, Colors.Blue.Darken3); // other events AddEvent(2, 11, 2, "Interview", Colors.Red.Lighten4, Colors.Red.Darken3); AddEvent(3, 12, 3, "Demo", Colors.Red.Lighten4, Colors.Red.Darken3); AddEvent(5, 5, 17, "PTO", Colors.Green.Lighten4, Colors.Green.Darken3); void AddEvent(uint day, uint hour, uint length, string name, Color backgroundColor, Color textColor) { table.Cell() .Column(day + 1).Row(hour).RowSpan(length) .Padding(5).Background(backgroundColor).Padding(5) .AlignCenter().AlignMiddle() .Text(name).FontColor(textColor); } }); }); }) .GenerateImages(x => "table-overlapping-cells.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void ManualCellPlacement() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(700, 1000)); page.DefaultTextStyle(x => x.FontSize(16 )); page.Margin(25); page.Content() .Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(75); columns.ConstantColumn(150); columns.ConstantColumn(200); columns.ConstantColumn(200); }); table.Cell().Row(1).Column(3).ColumnSpan(2) .Element(HeaderCellStyle) .Text("Predicted condition").Bold(); table.Cell().Row(3).Column(1).RowSpan(2) .Element(HeaderCellStyle).RotateLeft() .Text("Actual\ncondition").Bold().AlignCenter(); table.Cell().Row(2).Column(3) .Element(HeaderCellStyle) .Text("Positive (PP)"); table.Cell().Row(2).Column(4) .Element(HeaderCellStyle) .Text("Negative (PN)"); table.Cell().Row(3).Column(2) .Element(HeaderCellStyle).Text("Positive (P)"); table.Cell().Row(4).Column(2) .Element(HeaderCellStyle) .Text("Negative (N)"); table.Cell() .Row(3).Column(3).Element(GoodCellStyle) .Text("True positive (TP)"); table.Cell() .Row(3).Column(4).Element(BadCellStyle) .Text("False negative (FN)"); table.Cell().Row(4).Column(3) .Element(BadCellStyle).Text("False positive (FP)"); table.Cell().Row(4).Column(4) .Element(GoodCellStyle).Text("True negative (TN)"); static IContainer CellStyle(IContainer container, Color color) => container.Border(1).Background(color).PaddingHorizontal(10).PaddingVertical(15).AlignCenter().AlignMiddle(); static IContainer HeaderCellStyle(IContainer container) => CellStyle(container, Colors.Grey.Lighten4 ); static IContainer GoodCellStyle(IContainer container) => CellStyle(container, Colors.Green.Lighten4).DefaultTextStyle(x => x.FontColor(Colors.Green.Darken2)); static IContainer BadCellStyle(IContainer container) => CellStyle(container, Colors.Red.Lighten4).DefaultTextStyle(x => x.FontColor(Colors.Red.Darken2)); }); }); }) .GenerateImages(x => "table-manual-cell-placement.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void ColumnsDefinition() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(700, 1000)); page.DefaultTextStyle(x => x.FontSize(16)); page.Margin(25); page.Content() .Width(450) .Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(150); columns.RelativeColumn(2); columns.RelativeColumn(3); }); table.Cell().ColumnSpan(3) .Background(Colors.Grey.Lighten2).Element(CellStyle) .Text("Total width: 450px"); table.Cell().Element(CellStyle).Text("Constant: 150px"); table.Cell().Element(CellStyle).Text("Relative: 2*"); table.Cell().Element(CellStyle).Text("Relative: 3*"); table.Cell().Element(CellStyle).Text("150px"); table.Cell().Element(CellStyle).Text("120px"); table.Cell().Element(CellStyle).Text("180px"); static IContainer CellStyle(IContainer container) => container.Border(1).Padding(10); }); }); }) .GenerateImages(x => "table-columns-definition.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void HeaderAndFooter() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 250)); page.DefaultTextStyle(x => x.FontSize(16)); page.Margin(25); page.Content() .Border(1) .BorderColor(Colors.Grey.Lighten1) .Table(table => { var pageSizes = new List<(string name, double width, double height)>() { ("Letter (ANSI A)", 8.5f, 11), ("Legal", 8.5f, 14), ("Ledger (ANSI B)", 11, 17), ("Tabloid (ANSI B)", 17, 11), ("ANSI C", 22, 17), ("ANSI D", 34, 22), ("ANSI E", 44, 34) }; const int inchesToPoints = 72; IContainer DefaultCellStyle(IContainer container, string backgroundColor) { return container .Border(1) .BorderColor(Colors.Grey.Lighten1) .Background(backgroundColor) .PaddingVertical(5) .PaddingHorizontal(10) .AlignCenter() .AlignMiddle(); } table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.ConstantColumn(80); columns.ConstantColumn(80); columns.ConstantColumn(80); columns.ConstantColumn(80); }); table.Header(header => { // please be sure to call the 'header' handler! header.Cell().RowSpan(2).Element(CellStyle).ExtendHorizontal().AlignLeft() .Text("Document type").Bold(); header.Cell().ColumnSpan(2).Element(CellStyle).Text("Inches").Bold(); header.Cell().ColumnSpan(2).Element(CellStyle).Text("Points").Bold(); header.Cell().Element(CellStyle).Text("Width"); header.Cell().Element(CellStyle).Text("Height"); header.Cell().Element(CellStyle).Text("Width"); header.Cell().Element(CellStyle).Text("Height"); // you can extend existing styles by creating additional methods IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.Grey.Lighten3); }); foreach (var page in pageSizes) { table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().Text(page.name); // inches table.Cell().Element(CellStyle).Text(page.width); table.Cell().Element(CellStyle).Text(page.height); // points table.Cell().Element(CellStyle).Text(page.width * inchesToPoints); table.Cell().Element(CellStyle).Text(page.height * inchesToPoints); IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.White).ShowOnce(); } }); }); }) .GenerateImages(x => $"table-header-and-footer-{x}.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/Text/ParagraphStyleExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.Text; public class ParagraphStyleExamples { [Test] public void DefaultTextStyle() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(400, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.DefaultTextStyle(x => x.Light().LetterSpacing(-0.1f).WordSpacing(0.1f)); text.Span("Changing typography settings helps creating "); text.Span("significant").LetterSpacing(0.2f).Black().BackgroundColor(Colors.Grey.Lighten2); text.Span(" visual contrast."); }); }); }) .GenerateImages(x => "text-paragraph-default-style.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void TextAlignment() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(400, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(20); column.Item() .Element(CellStyle) .Text("This is an example of left-aligned text, showcasing how the text starts from the left margin and continues naturally across the container.") .AlignLeft(); column.Item() .Element(CellStyle) .Text("This text is centered within its container, creating a balanced look, especially for titles or headers.") .AlignCenter(); column.Item() .Element(CellStyle) .Text("This example demonstrates right-aligned text, often used for dates, numbers, or aligning text to the right margin.") .AlignRight(); column.Item() .Element(CellStyle) .Text("Justified text adjusts the spacing between words so that both the left and right edges of the text block are aligned, creating a clean, newspaper-like look.") .Justify(); static IContainer CellStyle(IContainer container) => container.Background(Colors.Grey.Lighten3).Padding(10); }); }); }) .GenerateImages(x => "text-paragraph-alignment.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void FirstLineIndentation() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1200)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(Placeholders.Paragraphs()) .ParagraphFirstLineIndentation(40); }); }) .GenerateImages(x => "text-paragraph-first-line-indentation.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } [Test] public void Spacing() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1200)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(Placeholders.Paragraphs()) .ParagraphSpacing(10); }); }) .GenerateImages(x => "text-paragraph-spacing.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.High, RasterDpi = 144 }); } [Test] public void ClampLines() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); var paragraph = Placeholders.Paragraph(); column.Item() .Background(Colors.Grey.Lighten3) .Padding(5) .Text(paragraph); column.Item() .Background(Colors.Grey.Lighten3) .Padding(5) .Text(paragraph) .ClampLines(3); }); }); }) .GenerateImages(x => "text-paragraph-clamp-lines.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void ClampLinesWithCustomEllipsis() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(Placeholders.Paragraph()) .ClampLines(3, " [...]"); }); }) .GenerateImages(x => "text-paragraph-clamp-lines-custom-ellipsis.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/Text/TextBasicExamples.cs ================================================ using System.Security.Cryptography; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.Text; public class TextBasicExamples { [Test] public void Basic() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text("Sample text"); }); }) .GenerateImages(x => "text-basic.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void BasicWithStyle() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); column.Item() .Element(CellStyle) .Text("Text with blue color") .FontColor(Colors.Blue.Darken1); column.Item() .Element(CellStyle) .Text("Bold and underlined text") .Bold() .Underline(); column.Item() .Element(CellStyle) .Text("Centered small text") .FontSize(12) .AlignCenter(); static IContainer CellStyle(IContainer container) => container.Background(Colors.Grey.Lighten3).Padding(10); }); }); }) .GenerateImages(x => "text-basic-descriptor.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void Rich() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.AlignCenter(); text.Span("The "); text.Span("chemical formula").Underline(); text.Span(" of "); text.Span("sulfuric acid").BackgroundColor(Colors.Amber.Lighten3); text.Span(" is H"); text.Span("2").Subscript(); text.Span("SO"); text.Span("4").Subscript(); text.Span("."); }); }); }) .GenerateImages(x => "text-rich.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void StyleInheritance() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .DefaultTextStyle(style => style.FontSize(20)) .Column(column => { column.Spacing(10); column.Item().Text("Products").ExtraBold().Underline().DecorationThickness(2); column.Item().Text("Comments: " + Placeholders.Sentence()); column.Item() .DefaultTextStyle(style => style.FontSize(14)) .Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(30); columns.RelativeColumn(1); columns.RelativeColumn(2); }); table.Header(header => { header.Cell().Element(Style).Text("ID"); header.Cell().Element(Style).Text("Name"); header.Cell().Element(Style).Text("Description"); IContainer Style(IContainer container) { return container .Background(Colors.Grey.Lighten3) .BorderBottom(1) .PaddingHorizontal(5) .PaddingVertical(10) .DefaultTextStyle(x => x.Bold().FontColor(Colors.Blue.Medium)); } }); foreach (var i in Enumerable.Range(0, 5)) { table.Cell().Element(Style).Text(i.ToString()).Bold(); table.Cell().Element(Style).Text(Placeholders.Label()); table.Cell().Element(Style).Text(Placeholders.Sentence()); } IContainer Style(IContainer container) { return container.Padding(5); } }); }); }); }) .GenerateImages(x => "text-style-inheritance.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void PageNumber() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Extend() .Placeholder(); page.Footer() .PaddingTop(25) .AlignCenter() .Text("3 / 10"); // .Text(text => // { // text.CurrentPageNumber(); // text.Span(" / "); // text.TotalPages(); // }); }); }) .GenerateImages(x => "text-page-number.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void PageNumberFormat() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A5); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.CurrentPageNumber().Format(FormatWithLeadingZeros); }); static string FormatWithLeadingZeros(int? pageNumber) { const int expectedLength = 3; pageNumber ??= 1; return pageNumber.Value.ToString($"D{expectedLength}"); } }); }) .GenerateImages(x => "text-page-number-format.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void Hyperlink() { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A6.Landscape()); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { var hyperlinkStyle = TextStyle.Default .FontColor(Colors.Blue.Medium) .Underline(); text.Span("To learn more about QuestPDF, please visit its "); text.Hyperlink("homepage", "https://www.questpdf.com/").Style(hyperlinkStyle); text.Span(", "); text.Hyperlink("GitHub repository", "https://github.com/QuestPDF/QuestPDF").Style(hyperlinkStyle); text.Span(" and "); text.Hyperlink("NuGet package page", "https://www.nuget.org/packages/QuestPDF").Style(hyperlinkStyle); text.Span("."); }); }); }) .GeneratePdf("text-hyperlink.pdf"); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/Text/TextInjectContent.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.Text; public class TextInjectContent { [Test] public void InjectImage() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("A unit test can either "); text.Element().PaddingBottom(-4).Height(24).Image("Resources/unit-test-completed-icon.png"); text.Span(" pass").FontColor(Colors.Green.Medium); text.Span(" or "); text.Element().PaddingBottom(-4).Height(24).Image("Resources/unit-test-failed-icon.png"); text.Span(" fail").FontColor(Colors.Red.Medium); text.Span("."); }); }); }) .GenerateImages(x => "text-inject-image.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void InjectSvg() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(350, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("To synchronize your email inbox, please click the "); text.Element().PaddingBottom(-4).Height(24).Svg("Resources/mail-synchronize-icon.svg"); text.Span(" icon."); }); }); }) .GenerateImages(x => "text-inject-svg.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void InjectPosition() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(400, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("This "); text.Element(TextInjectedElementAlignment.AboveBaseline) .Width(12).Height(12) .Background(Colors.Green.Medium); text.Span(" element is positioned above the baseline, while this "); text.Element(TextInjectedElementAlignment.BelowBaseline) .Width(12).Height(12) .Background(Colors.Blue.Medium); text.Span(" element is positioned below the baseline."); }); }); }) .GenerateImages(x => "text-inject-position.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/Text/TextStyleExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples.Text; public class TextStyleExamples { [Test] public void FontSize() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); column.Item() .Text("This is small text (16pt)") .FontSize(16); column.Item() .Text("This is medium text (24pt)") .FontSize(24); column.Item() .Text("This is large text (36pt)") .FontSize(36); }); }); }) .GenerateImages(x => "text-font-size.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void FontFamily() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(10); column.Item().Text("This is text with default font (Lato)"); column.Item().Text("This is text with Times New Roman font") .FontFamily("Times New Roman"); column.Item().Text("This is text with Courier New font") .FontFamily("Courier New"); }); }); }) .GenerateImages(x => "text-font-family.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } [Test] public void FontColor() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("Each pixel consists of three sub-pixels: "); text.Span("red").FontColor(Colors.Red.Medium); text.Span(", "); text.Span("green").FontColor(Colors.Green.Medium); text.Span(" and "); text.Span("blue").FontColor(Colors.Blue.Medium); text.Span("."); }); }); }) .GenerateImages(x => "text-font-color.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void BackgroundColor() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("The term "); text.Span("algorithm").BackgroundColor(Colors.Yellow.Lighten3).Bold(); text.Span(" refers to a set of rules or steps used to solve a problem."); }); }); }) .GenerateImages(x => "text-font-background.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Italic() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("In this sentence, the word "); text.Span("important").Italic(); text.Span(" is emphasized using italics."); }); }); }) .GenerateImages(x => "text-font-italic.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void FontWeight() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("This sentence demonstrates "); text.Span("bold").Bold(); text.Span(", "); text.Span("normal").NormalWeight(); text.Span(", "); text.Span("light").Light(); text.Span(" and "); text.Span("thin").Thin(); text.Span(" font weights."); }); }); }) .GenerateImages(x => "text-font-weight.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Subscript() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("H"); text.Span("2").Subscript(); text.Span("O is the chemical formula for water."); }); }); }) .GenerateImages(x => "text-subscript.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void Superscript() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("E = mc"); text.Span("2").Superscript(); text.Span(" is the equation of mass-energy equivalence."); }); }); }) .GenerateImages(x => "text-superscript.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void LineHeight() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(20); float[] lineHeights = [0.75f, 1f, 2f]; var paragraph = Placeholders.Paragraph(); foreach (var lineHeight in lineHeights) { column .Item() .Background(Colors.Grey.Lighten3) .Padding(5) .Text(paragraph) .FontSize(16) .LineHeight(lineHeight); } }); }); }) .GenerateImages(x => "text-line-height.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void LetterSpacing() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(20); var letterSpacing = new[] { -0.08f, 0f, 0.2f }; var paragraph = Placeholders.Sentence(); foreach (var spacing in letterSpacing) { column .Item() .Background(Colors.Grey.Lighten3) .Padding(5) .Text(paragraph) .FontSize(18) .LetterSpacing(spacing); } }); }); }) .GenerateImages(x => "text-letter-spacing.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void WordSpacing() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Column(column => { column.Spacing(20); var wordSpacing = new[] { -0.2f, 0f, 0.4f }; var paragraph = Placeholders.Sentence(); foreach (var spacing in wordSpacing) { column.Item() .Background(Colors.Grey.Lighten3) .Padding(5) .Text(paragraph) .FontSize(16) .WordSpacing(spacing); } }); }); }) .GenerateImages(x => "text-word-spacing.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void FontFallback() { Settings.UseEnvironmentFonts = false; Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text("The Arabic word for programming is البرمجة.") .FontFamily("Lato", "Noto Sans Arabic"); }); }) .GenerateImages(x => "text-font-fallback.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void FontFallbackEmoji() { Settings.UseEnvironmentFonts = false; Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(600, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text("Popular emojis include 😊, 😂, ❤️, 👍, and 😎.") .FontFamily("Lato", "Noto Emoji"); }); }) .GenerateImages(x => "text-font-fallback-emoji.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void TextFontFeatures() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Row(row => { row.Spacing(25); row.RelativeItem() .Background(Colors.Grey.Lighten3) .Padding(10) .Column(column => { column.Item().Text("Without ligatures").FontSize(16); column.Item() .Text("fly and fight") .FontSize(32) .DisableFontFeature(FontFeatures.StandardLigatures); }); row.RelativeItem() .Background(Colors.Grey.Lighten3) .Padding(10) .Column(column => { column.Item().Text("With ligatures").FontSize(16); column.Item().Text("fly and fight") .FontSize(32) .EnableFontFeature(FontFeatures.StandardLigatures); }); }); }); }) .GenerateImages(x => "text-font-features.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void DecorationTypes() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("There are a couple of available text decorations: "); text.Span("underline").Underline().FontColor(Colors.Red.Medium); text.Span(", "); text.Span("strikethrough").Strikethrough().FontColor(Colors.Green.Medium); text.Span(" and "); text.Span("overline").Overline().FontColor(Colors.Blue.Medium); text.Span(". "); }); }); }) .GenerateImages(x => "text-decoration-types.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void DecorationStyles() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("Moreover, the decoration can be "); text.Span("solid").FontColor(Colors.Indigo.Medium).Underline().DecorationSolid(); text.Span(", "); text.Span("double").FontColor(Colors.Blue.Medium).Underline().DecorationDouble(); text.Span(", "); text.Span("wavy").FontColor(Colors.LightBlue.Medium).Underline().DecorationWavy(); text.Span(", "); text.Span("dotted").FontColor(Colors.Cyan.Medium).Underline().DecorationDotted(); text.Span(" or "); text.Span("dashed").FontColor(Colors.Green.Medium) .Underline().DecorationDashed(); text.Span("."); }); }); }) .GenerateImages(x => "text-decoration-styles.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } [Test] public void DecorationsAdvanced() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(500, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .Text(text => { text.Span("This text contains a "); text.Span("seriuos") .Underline() .DecorationWavy() .DecorationColor(Colors.Red.Medium) .DecorationThickness(2); text.Span(" typo."); }); }); }) .GenerateImages(x => "text-decoration-advanced.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.Best, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/TranslateExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class TranslateExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(400, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Padding(50) .Background(Colors.Blue.Lighten3) .TranslateX(25) .TranslateY(25) .Border(4) .BorderColor(Colors.Blue.Darken2) .Padding(50) .Text("Moved content") .FontSize(25); }); }) .GenerateImages(x => "translate.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/UnconstrainedExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class UnconstrainedExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() .Width(400) .Height(350) .Padding(25) .PaddingLeft(50) .Column(column => { column.Item().Width(300).Height(150).Background(Colors.Blue.Lighten3); column .Item() .Unconstrained() .TranslateX(-50) .TranslateY(-50) .Width(100) .Height(100) .Background(Colors.Blue.Darken2); column.Item().Width(300).Height(150).Background(Colors.Blue.Lighten2); }); }); }) .GenerateImages(x => "unconstrained.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.DocumentationExamples/ZIndexExamples.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.DocumentationExamples; public class ZIndexExamples { [Test] public void Example() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(650, 0)); page.MaxSize(new PageSize(650, 1000)); page.DefaultTextStyle(x => x.FontSize(20)); page.Margin(25); page.Content() .PaddingVertical(15) .Border(2) .Row(row => { row.RelativeItem() .Background(Colors.Grey.Lighten3) .Element(c => AddPricingItem(c, "Community", "Free")); row.RelativeItem() .ZIndex(1) // -1 or 0 or 1 .Padding(-15) .Border(1) .Background(Colors.Grey.Lighten1) .PaddingTop(15) .Element(c => AddPricingItem(c, "Professional", "$699")); row.RelativeItem() .Background(Colors.Grey.Lighten3) .Element(c => AddPricingItem(c, "Enterprise", "$1999")); void AddPricingItem(IContainer container, string name, string formattedPrice) { container .Padding(25) .Column(column => { column.Item().AlignCenter().Text(name).FontSize(24).Black(); column.Item().AlignCenter().Text(formattedPrice).FontSize(20).SemiBold(); column.Item().PaddingHorizontal(-25).PaddingVertical(10).LineHorizontal(1); foreach (var i in Enumerable.Range(1, 4)) { column.Item() .PaddingTop(10) .AlignCenter() .Text(Placeholders.Label()) .FontSize(16) .Light(); } }); } }); }); }) .GenerateImages(x => "zindex-positive.webp", new ImageGenerationSettings() { ImageFormat = ImageFormat.Webp, ImageCompressionQuality = ImageCompressionQuality.VeryHigh, RasterDpi = 144 }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/ColumnTests.cs ================================================ namespace QuestPDF.LayoutTests; public class ColumnTests { [Test] public void Typical() { LayoutTest .HavingSpaceOfSize(100, 140) .ForContent(content => { content.Shrink().Column(column => { column.Spacing(10); column.Item().Mock("a").ContinuousBlock(50, 30); column.Item().Mock("b").ContinuousBlock(40, 20); column.Item().Mock("c").ContinuousBlock(70, 40); column.Item().Mock("d").ContinuousBlock(60, 60); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(70, 140) .Content(page => { page.Mock("a").Position(0, 0).Size(70, 30); page.Mock("b").Position(0, 40).Size(70, 20); page.Mock("c").Position(0, 70).Size(70, 40); page.Mock("d").Position(0, 120).Size(70, 20); }); document .Page() .RequiredAreaSize(60, 40) .Content(page => { page.Mock("d").Position(0, 0).Size(60, 40); }); }); } [Test] public void SingleItem() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Column(column => { column.Spacing(10); column.Item().Mock("a").ContinuousBlock(50, 30); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(50, 30) .Content(page => { page.Mock("a").Position(0, 0).Size(50, 30); }); }); } [Test] public void ZeroHeightItemDoesNotConsumeSpacing() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Column(column => { column.Spacing(10); column.Item().Mock("a").ContinuousBlock(50, 30); column.Item().Mock("b").ContinuousBlock(50, 0); column.Item().Mock("c").ContinuousBlock(70, 20); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(70, 60) .Content(page => { page.Mock("a").Position(0, 0).Size(70, 30); page.Mock("b").Position(0, 30).Size(70, 0); page.Mock("c").Position(0, 40).Size(70, 20); }); }); } [Test] public void NoSpacing() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Column(column => { column.Spacing(0); column.Item().Mock("a").ContinuousBlock(50, 30); column.Item().Mock("b").ContinuousBlock(40, 20); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(50, 50) .Content(page => { page.Mock("a").Position(0, 0).Size(50, 30); page.Mock("b").Position(0, 30).Size(50, 20); }); }); } [Test] public void PartialRenderItem() { LayoutTest .HavingSpaceOfSize(100, 80) .ForContent(content => { content.Shrink().Column(column => { column.Spacing(5); column.Item().Mock("a").ContinuousBlock(50, 40); column.Item().Mock("b").ContinuousBlock(60, 50); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(60, 80) .Content(page => { page.Mock("a").Position(0, 0).Size(60, 40); page.Mock("b").Position(0, 45).Size(60, 35); }); document .Page() .RequiredAreaSize(60, 15) .Content(page => { page.Mock("b").Position(0, 0).Size(60, 15); }); }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/LineTests.cs ================================================ namespace QuestPDF.LayoutTests; public class LineTests { [Test] public void VerticalLineRequiresSpaceEqualToItsThickness() { LayoutTest .HavingSpaceOfSize(5, 100) .ForContent(content => { content.Shrink().LineVertical(10); }) .ExpectLayoutException("The line thickness is greater than the available horizontal space."); } [Test] public void HorizontalLineRequiresSpaceEqualToItsThickness() { LayoutTest .HavingSpaceOfSize(100, 5) .ForContent(content => { content.Shrink().LineHorizontal(10); }) .ExpectLayoutException("The line thickness is greater than the available vertical space."); } #region Stateful [Test] public void CheckRenderingState() { LayoutTest .HavingSpaceOfSize(240, 100) .ForContent(content => { content.ShrinkVertical().Mock("a").LineHorizontal(2); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(0, 2) .Content(page => { page.Mock("a").Position(0, 0).Size(240, 2).State(true); }); }); } #endregion } ================================================ FILE: Source/QuestPDF.LayoutTests/MultiColumnTests.cs ================================================ using QuestPDF.Elements; using QuestPDF.Helpers; namespace QuestPDF.LayoutTests; public class MultiColumnTests { [Test] public void DynamicComponent() { LayoutTest .HavingSpaceOfSize(400, 200) .ForContent(content => { content .Shrink() .MultiColumn(column => { column.Content() .Mock("dynamic") .Dynamic(new CounterComponent()); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(400, 50) .Content(page => { page.Mock("dynamic") .Position(0, 0) .Size(200, 50) .State(new DynamicHost.DynamicState() { IsRendered = false, RenderCount = 1, ChildState = 2 }); page.Mock("dynamic") .Position(200, 0) .Size(200, 50) .State(new DynamicHost.DynamicState() { IsRendered = false, RenderCount = 2, ChildState = 3 }); }); document .Page() .RequiredAreaSize(400, 50) .Content(page => { page.Mock("dynamic") .Position(0, 0) .Size(200, 50) .State(new DynamicHost.DynamicState() { IsRendered = false, RenderCount = 3, ChildState = 4 }); page.Mock("dynamic") .Position(200, 0) .Size(200, 50) .State(new DynamicHost.DynamicState() { IsRendered = false, RenderCount = 4, ChildState = 5 }); }); document .Page() .RequiredAreaSize(400, 50) .Content(page => { page.Mock("dynamic") .Position(0, 0) .Size(200, 50) .State(new DynamicHost.DynamicState() { IsRendered = true, RenderCount = 5, ChildState = 6 }); }); }); } public class CounterComponent : IDynamicComponent { public int State { get; set; } = 1; public DynamicComponentComposeResult Compose(DynamicContext context) { var content = context.CreateElement(element => { element .Width(100) .Height(50) .Background(Colors.Grey.Lighten2) .AlignCenter() .AlignMiddle() .Text($"Item {State}") .SemiBold(); }); State++; return new DynamicComponentComposeResult { Content = content, HasMoreContent = State <= 5 }; } } } ================================================ FILE: Source/QuestPDF.LayoutTests/PaddingTests.cs ================================================ namespace QuestPDF.LayoutTests; [TestFixture] public class PaddingTests { [Test] public void PaddingModifiesPositioningAndMinimumSize() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content .Shrink() .PaddingLeft(5) .PaddingTop(10) .PaddingRight(15) .PaddingBottom(20) .Mock("a") .SolidBlock(20, 30); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(40, 60) .Content(page => { page.Mock("a").Position(5, 10).Size(20, 30); }); }); } [Test] public void NegativePaddingIsAllowed() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content .Shrink() .Padding(-10) .Mock("a") .SolidBlock(50, 70); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(30, 50) .Content(page => { page.Mock("a").Position(-10, -10).Size(50, 70); }); }); } [Test] public void PaddingSupportsPaging() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content .Shrink() .Padding(15) .Mock("a") .ContinuousBlock(50, 90); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(80, 100) .Content(page => { page.Mock("a").Position(15, 15).Size(50, 70); }); document .Page() .RequiredAreaSize(80, 50) .Content(page => { page.Mock("a").Position(15, 15).Size(50, 20); }); }); } [Test] public void MultipleItemsWithAppliedPadding() { LayoutTest .HavingSpaceOfSize(100, 150) .ForContent(content => { content.Shrink().Column(column => { column.Item().PaddingVertical(5).Mock("a").SolidBlock(15, 25); column.Item().PaddingHorizontal(10).Mock("b").SolidBlock(20, 30); column.Item().Padding(15).Mock("c").SolidBlock(25, 35); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(55, 130) .Content(page => { page.Mock("a").Position(0, 5).Size(55, 25); page.Mock("b").Position(10, 35).Size(35, 30); page.Mock("c").Position(15, 80).Size(25, 35); }); }); } [Test] public void PaddingProducesAvailableSpaceOfNegativeSize() { LayoutTest .HavingSpaceOfSize(100, 150) .ForContent(content => { content.Shrink().Padding(60).SolidBlock(20, 25); }) .ExpectLayoutException("The available space is negative."); } [Test] public void PaddingWithEmptyChild() { LayoutTest .HavingSpaceOfSize(100, 150) .ForContent(content => { content.Shrink().Padding(30); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(60, 60); }); } [Test] public void PaddingOnEmptyElementProducesAvailableSpaceOfNegativeSize() { LayoutTest .HavingSpaceOfSize(100, 150) .ForContent(content => { content.Shrink().Padding(60); }) .ExpectLayoutException("The available space is negative."); } [Test] public void PaddingOnEmptyElementProducesAvailableSpaceOfNegativeSize2() { LayoutTest .HavingSpaceOfSize(100, 150) .ForContent(content => { content.Shrink().Padding(60).Column(column => { }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(0, 0); }); } [Test] public void NegativePaddingProducesMeasurementOfNegativeSize() { LayoutTest .HavingSpaceOfSize(100, 150) .ForContent(content => { content.Shrink().Padding(-15).Mock("a").SolidBlock(20, 40); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(0, 10) .Content(page => { page.Mock("a").Position(-15, -15).Size(30, 40); }); }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/QuestPDF.LayoutTests.csproj ================================================ net10.0 enable enable en false all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: Source/QuestPDF.LayoutTests/RotateTests.cs ================================================ namespace QuestPDF.LayoutTests; public class RotateTests { [Test] public void SimpleRotation() { const float angle = 60; const float armLength = 100; var expectedX = armLength * MathF.Cos(float.DegreesToRadians(angle)); var expectedY = armLength * MathF.Sin(float.DegreesToRadians(angle)); Assert.That(expectedX, Is.EqualTo(50).Within(0.1f)); Assert.That(expectedY, Is.EqualTo(86.6).Within(0.1f)); LayoutTest .HavingSpaceOfSize(500, 500) .ForContent(content => { content .Shrink() .Rotate(angle) .TranslateX(armLength) .Mock("a") .SolidBlock(100, 100); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 100) .Content(page => { page.Mock("a").Position(expectedX, expectedY).Size(100, 100); }); }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/RowTests.cs ================================================ namespace QuestPDF.LayoutTests; [TestFixture] public class RowTests { #region General Item Positioning [Test] public void SingleConstantItem() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(40).Mock("a").Height(30); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(40, 30) .Content(page => { page.Mock("a").Position(0, 0).Size(40, 30); }); }); } [Test] public void MultipleConstantItems() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(20).Mock("a").Height(30); row.ConstantItem(30).Mock("b").Height(40); row.ConstantItem(40).Mock("c").Height(20); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(90, 40) .Content(page => { page.Mock("a").Position(0, 0).Size(20, 40); page.Mock("b").Position(20, 0).Size(30, 40); page.Mock("c").Position(50, 0).Size(40, 40); }); }); } [Test] public void SingleRelativeItem() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.RelativeItem().Mock("a").Height(30); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 30) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 30); }); }); } [Test] public void TwoRelativeItems() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.RelativeItem(2).Mock("a").Height(40); row.RelativeItem(3).Mock("b").Height(50); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 50) .Content(page => { page.Mock("a").Position(0, 0).Size(40, 50); page.Mock("b").Position(40, 0).Size(60, 50); }); }); } [Test] public void SingleAutoItem() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.AutoItem().Mock("a").Size(60, 40); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(60, 40) .Content(page => { page.Mock("a").Position(0, 0).Size(60, 40); }); }); } [Test] public void RelativeItemFillsRemainingSpace() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(20).Mock("a").Height(30); row.ConstantItem(30).Mock("b").Height(50); row.RelativeItem().Mock("c").Height(40); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 50) .Content(page => { page.Mock("a").Position(0, 0).Size(20, 50); page.Mock("b").Position(20, 0).Size(30, 50); page.Mock("c").Position(50, 0).Size(50, 50); }); }); } [Test] public void RelativeItemsSplitRemainingSpaceAccordingToProportions() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(30).Mock("a").Height(60); row.RelativeItem(4).Mock("b").Height(40); row.RelativeItem(3).Mock("c").Height(30); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 60) .Content(page => { page.Mock("a").Position(0, 0).Size(30, 60); page.Mock("b").Position(30, 0).Size(40, 60); page.Mock("c").Position(70, 0).Size(30, 60); }); }); } [Test] public void ComplexItems() { LayoutTest .HavingSpaceOfSize(200, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(30).Mock("a").Height(60); row.RelativeItem(1).Mock("b").Height(40); row.RelativeItem(2).Mock("c").Height(30); row.ConstantItem(20).Mock("d").Height(30); row.RelativeItem(3).Mock("e").Height(20); row.RelativeItem(2).Mock("f").Height(70); row.AutoItem().Mock("g").Size(40, 50); row.AutoItem().Mock("h").Size(30, 40); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(200, 70) .Content(page => { page.Mock("a").Position(0, 0).Size(30, 70); page.Mock("b").Position(30, 0).Size(10, 70); page.Mock("c").Position(40, 0).Size(20, 70); page.Mock("d").Position(60, 0).Size(20, 70); page.Mock("e").Position(80, 0).Size(30, 70); page.Mock("f").Position(110, 0).Size(20, 70); page.Mock("g").Position(130, 0).Size(40, 70); page.Mock("h").Position(170, 0).Size(30, 70); }); }); } #endregion #region Drawing On Multiple Pages [Test] public void OneItemSpansTwoPages() { LayoutTest .HavingSpaceOfSize(80, 100) .ForContent(content => { content.Shrink().Row(row => { row.RelativeItem().Mock("a").SolidBlock(height: 40); row.RelativeItem().Mock("b").SolidBlock(height: 80); row.RelativeItem(2).Mock("c").ContinuousBlock(20, 140); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(80, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(20, 100); page.Mock("b").Position(20, 0).Size(20, 100); page.Mock("c").Position(40, 0).Size(40, 100); }); document .Page() .RequiredAreaSize(80, 40) .Content(page => { page.Mock("a").Position(0, 0).Size(20, 40); page.Mock("b").Position(20, 0).Size(20, 40); page.Mock("c").Position(40, 0).Size(40, 40); }); }); } #endregion #region Spacing [Test] public void NoSpacing() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(10).Mock("a").SolidBlock(height: 40); row.ConstantItem(20).Mock("b").SolidBlock(height: 50); row.ConstantItem(30).Mock("c").SolidBlock(height: 30); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(60, 50) .Content(page => { page.Mock("a").Position(0, 0).Size(10, 50); page.Mock("b").Position(10, 0).Size(20, 50); page.Mock("c").Position(30, 0).Size(30, 50); }); }); } [Test] public void NormalSpacing() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.Spacing(10); row.ConstantItem(10).Mock("a").SolidBlock(height: 40); row.ConstantItem(20).Mock("b").SolidBlock(height: 50); row.ConstantItem(30).Mock("c").SolidBlock(height: 30); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(80, 50) .Content(page => { page.Mock("a").Position(0, 0).Size(10, 50); page.Mock("b").Position(20, 0).Size(20, 50); page.Mock("c").Position(50, 0).Size(30, 50); }); }); } [Test] public void BiggerSpacing() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.Spacing(15); row.ConstantItem(10).Mock("a").SolidBlock(height: 40); row.ConstantItem(20).Mock("b").SolidBlock(height: 50); row.ConstantItem(30).Mock("c").SolidBlock(height: 30); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(90, 50) .Content(page => { page.Mock("a").Position(0, 0).Size(10, 50); page.Mock("b").Position(25, 0).Size(20, 50); page.Mock("c").Position(60, 0).Size(30, 50); }); }); } [Test] public void SpacingDoesNotFitInAvailableSpace() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.Spacing(20); row.ConstantItem(30).Height(50); row.ConstantItem(30).Height(50); row.ConstantItem(30).Height(50); }); }) .ExpectLayoutException("The content requires more horizontal space than available."); } [Test] public void SpacingIsLargerThanAvailableSpace() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.Spacing(200); row.ConstantItem(10).SolidBlock(height: 40); // <- }); }) .ExpectLayoutException("The content requires more horizontal space than available."); } [Test] public void NotEnoughSpaceForRelativeItemsAfterApplyingSpacing() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.Spacing(20); row.ConstantItem(40); row.ConstantItem(40); row.RelativeItem(); }); }) .ExpectLayoutException("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."); } #endregion #region Paging [Test] public void OneItemRequiresTwoPages() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.RelativeItem(2).Mock("a").Height(30); row.RelativeItem(3).Mock("b").ContinuousBlock(height: 140); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(40, 100); page.Mock("b").Position(40, 0).Size(60, 100); }); document .Page() .RequiredAreaSize(100, 40) .Content(page => { page.Mock("a").Position(0, 0).Size(40, 40); page.Mock("b").Position(40, 0).Size(60, 40); }); }); } [Test] public void ItemsRequireMultiplePages() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.RelativeItem(2).Mock("a").ContinuousBlock(height: 230); row.RelativeItem(3).Mock("b").ContinuousBlock(height: 140); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(40, 100); page.Mock("b").Position(40, 0).Size(60, 100); }); document .Page() .RequiredAreaSize(100, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(40, 100); page.Mock("b").Position(40, 0).Size(60, 100); }); document .Page() .RequiredAreaSize(100, 30) .Content(page => { page.Mock("a").Position(0, 0).Size(40, 30); page.Mock("b").Position(40, 0).Size(60, 30); }); }); } [Test] public void ItemHeightExceedsAvailableHeight() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(40).Height(50); row.ConstantItem(40).Height(200); // <- }); }) .ExpectLayoutException("The available vertical space is less than the minimum height."); } #endregion #region Right-To-Left Direction [Test] public void RightToLeftDirection() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.ContentFromRightToLeft().Shrink().Row(row => { row.ConstantItem(20).Mock("a").Height(30); row.ConstantItem(30).Mock("b").Height(40); row.ConstantItem(40).Mock("c").Height(20); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(90, 40) .Content(page => { page.Mock("a").Position(80, 0).Size(20, 40); page.Mock("b").Position(50, 0).Size(30, 40); page.Mock("c").Position(10, 0).Size(40, 40); }); }); } #endregion #region Layout Exceptions [Test] public void ConstantItemOfTooLargeSize() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(200); // <- }); }) .ExpectLayoutException("The content requires more horizontal space than available."); } [Test] public void ConstantItemHasChildOfTooLargeSize() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(30).Width(20); row.ConstantItem(40).Width(200); // <- }); }) .ExpectLayoutException("The available horizontal space is less than the minimum width."); } [Test] public void SumOfConstantItemsExceedsAvailableWidth() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(40); row.ConstantItem(40); row.ConstantItem(40); }); }) .ExpectLayoutException("The content requires more horizontal space than available."); } [Test] public void RelativeItemHasChildOfTooLargeSize() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.RelativeItem(2).Width(30); row.RelativeItem(3).Width(200); // <- }); }) .ExpectLayoutException("The available horizontal space is less than the minimum width."); } [Test] public void AutoItemHasChildOfTooLargeSize() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(30); row.AutoItem().Width(80); }); }) .ExpectLayoutException("The content requires more horizontal space than available."); } [Test] public void SumOfAutoItemsExceedsAvailableWidth() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.AutoItem().Width(30); row.AutoItem().Width(40); row.AutoItem().Width(50); }); }) .ExpectLayoutException("The content requires more horizontal space than available."); } [Test] public void SumOfVariousItemsExceedsAvailableWidth() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.ConstantItem(60); row.AutoItem().Width(50); }); }) .ExpectLayoutException("The content requires more horizontal space than available."); } #endregion #region Corner Cases [Test] public void NoItems() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { // <- }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(0, 0) .Content(page => { }); }); } [Test] public void RerenderingOfFullyDrawnRow() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink().Row(row => { row.RelativeItem().Mock("a").Row(innerRow => { innerRow.RelativeItem().Mock("b").SolidBlock(20, 50); innerRow.RelativeItem().Mock("c").SolidBlock(15, 40); }); row.RelativeItem().Mock("d").ContinuousBlock(40, 140); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(50, 100); page.Mock("b").Position(0, 0).Size(25, 100); page.Mock("c").Position(25, 0).Size(25, 100); page.Mock("d").Position(50, 0).Size(50, 100); }); document .Page() .RequiredAreaSize(100, 40) .Content(page => { page.Mock("a").Position(0, 0).Size(50, 40); page.Mock("d").Position(50, 0).Size(50, 40); }); }); } #endregion #region Stateful [Test] public void CheckRenderingState() { LayoutTest .HavingSpaceOfSize(240, 100) .ForContent(content => { content.Shrink().Mock("a").Row(innerRow => { innerRow.RelativeItem().ContinuousBlock(50, 80); innerRow.RelativeItem().ContinuousBlock(50, 250); innerRow.RelativeItem().ContinuousBlock(50, 170); innerRow.RelativeItem().ContinuousBlock(50, 320); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(240, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(240, 100).State(new[] { true, false, false, false }); }); document .Page() .RequiredAreaSize(240, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(240, 100).State(new[] { true, false, true, false }); }); document .Page() .RequiredAreaSize(240, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(240, 100).State(new[] { true, true, true, false }); }); document .Page() .RequiredAreaSize(240, 20) .Content(page => { page.Mock("a").Position(0, 0).Size(240, 20).State(new[] { true, true, true, true }); }); }); } #endregion } ================================================ FILE: Source/QuestPDF.LayoutTests/ScaleTests.cs ================================================ using QuestPDF.Helpers; namespace QuestPDF.LayoutTests; public class ScaleTests { private void DrawTestSubject(IContainer container) { container .Inlined(inlined => { inlined.Item().Mock("a").SolidBlock(100, 100); inlined.Item().Mock("b").SolidBlock(100, 100); inlined.Item().Mock("c").SolidBlock(100, 100); inlined.Item().Mock("d").SolidBlock(100, 100); inlined.Item().Mock("e").SolidBlock(100, 100); inlined.Item().Mock("f").SolidBlock(100, 100); }); } [Test] public void DefaultScale() { LayoutTest .HavingSpaceOfSize(800, 500) .ForContent(content => { content.Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(600, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(100, 0).Size(100, 100); page.Mock("c").Position(200, 0).Size(100, 100); page.Mock("d").Position(300, 0).Size(100, 100); page.Mock("e").Position(400, 0).Size(100, 100); page.Mock("f").Position(500, 0).Size(100, 100); }); }); } [Test] public void PositiveScale05() { LayoutTest .HavingSpaceOfSize(800, 500) .ForContent(content => { content.Scale(0.5f).Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(300, 50) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(50, 0).Size(100, 100); page.Mock("c").Position(100, 0).Size(100, 100); page.Mock("d").Position(150, 0).Size(100, 100); page.Mock("e").Position(200, 0).Size(100, 100); page.Mock("f").Position(250, 0).Size(100, 100); }); }); } [Test] public void PositiveScale15() { LayoutTest .HavingSpaceOfSize(800, 500) .ForContent(content => { content.Scale(1.5f).Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(750, 300) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(150, 0).Size(100, 100); page.Mock("c").Position(300, 0).Size(100, 100); page.Mock("d").Position(450, 0).Size(100, 100); page.Mock("e").Position(600, 0).Size(100, 100); page.Mock("f").Position(0, 150).Size(100, 100); }); }); } [Test] public void PositiveScale25() { LayoutTest .HavingSpaceOfSize(800, 500) .ForContent(content => { content.Scale(2.5f).Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(750, 500) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(250, 0).Size(100, 100); page.Mock("c").Position(500, 0).Size(100, 100); page.Mock("d").Position(0, 250).Size(100, 100); page.Mock("e").Position(250, 250).Size(100, 100); page.Mock("f").Position(500, 250).Size(100, 100); }); }); } [Test] public void PositiveScaleTwoPages() { LayoutTest .HavingSpaceOfSize(800, 250) .ForContent(content => { content.Scale(2).Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(800, 200) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(200, 0).Size(100, 100); page.Mock("c").Position(400, 0).Size(100, 100); page.Mock("d").Position(600, 0).Size(100, 100); }); document .Page() .RequiredAreaSize(400, 200) .Content(page => { page.Mock("e").Position(0, 0).Size(100, 100); page.Mock("f").Position(200, 0).Size(100, 100); }); }); } [Test] public void ScaleVertical() { LayoutTest .HavingSpaceOfSize(450, 800) .ForContent(content => { content.ScaleVertical(2f).Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(400, 400) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(100, 0).Size(100, 100); page.Mock("c").Position(200, 0).Size(100, 100); page.Mock("d").Position(300, 0).Size(100, 100); page.Mock("e").Position(0, 200).Size(100, 100); page.Mock("f").Position(100, 200).Size(100, 100); }); }); } [Test] public void ScaleVerticalNegative() { LayoutTest .HavingSpaceOfSize(400, 500) .ForContent(content => { content.ScaleVertical(-1.5f).Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(400, 300) .Content(page => { page.Mock("a").Position(0, 500).Size(100, 100); page.Mock("b").Position(100, 500).Size(100, 100); page.Mock("c").Position(200, 500).Size(100, 100); page.Mock("d").Position(300, 500).Size(100, 100); page.Mock("e").Position(0, 350).Size(100, 100); page.Mock("f").Position(100, 350).Size(100, 100); }); }); } [Test] public void ScaleHorizontal() { LayoutTest .HavingSpaceOfSize(450, 800) .ForContent(content => { content.ScaleHorizontal(2f).Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(400, 300) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(200, 0).Size(100, 100); page.Mock("c").Position(0, 100).Size(100, 100); page.Mock("d").Position(200, 100).Size(100, 100); page.Mock("e").Position(0, 200).Size(100, 100); page.Mock("f").Position(200, 200).Size(100, 100); }); }); } [Test] public void ScaleHorizontalNegative() { LayoutTest .HavingSpaceOfSize(700, 400) .ForContent(content => { content.ScaleHorizontal(-1.5f).Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(600, 200) .Content(page => { page.Mock("a").Position(700, 0).Size(100, 100); page.Mock("b").Position(550, 0).Size(100, 100); page.Mock("c").Position(400, 0).Size(100, 100); page.Mock("d").Position(250, 0).Size(100, 100); page.Mock("e").Position(700, 100).Size(100, 100); page.Mock("f").Position(550, 100).Size(100, 100); }); }); } [Test] public void WrapHorizontal() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Scale(3).Width(50).Height(10); }) .ExpectLayoutException("The available horizontal space is less than the minimum width."); } [Test] public void WrapVertical() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Scale(3).Width(10).Height(50); }) .ExpectLayoutException("The available vertical space is less than the minimum height."); } } ================================================ FILE: Source/QuestPDF.LayoutTests/Setup.cs ================================================ namespace QuestPDF.LayoutTests { [SetUpFixture] public class Setup { [OneTimeSetUp] public static void Configure() { QuestPDF.Settings.License = LicenseType.Community; } } } ================================================ FILE: Source/QuestPDF.LayoutTests/ShowIfTests.cs ================================================ namespace QuestPDF.LayoutTests; public class ShowIfTests { [Test] public void Scenario() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Decoration(decoration => { decoration.Before().ShowIf(c => c.PageNumber % 2 == 0).Mock("before").Size(80, 20); decoration.Content().Mock("content").ContinuousBlock(70, 460); decoration.After().ShowIf(c => c.PageNumber % 3 == 0).Mock("after").Size(90, 30); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(70, 100) .Content(page => { page.Mock("content").Position(0, 0).Size(70, 100); }); document .Page() .RequiredAreaSize(80, 100) .Content(page => { page.Mock("before").Position(0, 0).Size(80, 20); page.Mock("content").Position(0, 20).Size(80, 80); }); document .Page() .RequiredAreaSize(90, 100) .Content(page => { page.Mock("content").Position(0, 0).Size(90, 70); page.Mock("after").Position(0, 70).Size(90, 30); }); document .Page() .RequiredAreaSize(80, 100) .Content(page => { page.Mock("before").Position(0, 0).Size(80, 20); page.Mock("content").Position(0, 20).Size(80, 80); }); document .Page() .RequiredAreaSize(70, 100) .Content(page => { page.Mock("content").Position(0, 0).Size(70, 100); }); document .Page() .RequiredAreaSize(90, 80) .Content(page => { page.Mock("before").Position(0, 0).Size(90, 20); page.Mock("content").Position(0, 20).Size(90, 30); page.Mock("after").Position(0, 50).Size(90, 30); }); }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/ShrinkTests.cs ================================================ namespace QuestPDF.LayoutTests; public class ShrinkTests { [Test] public void Both() { LayoutTest .HavingSpaceOfSize(100, 120) .ForContent(content => { content .Shrink() .Mock("a").ContinuousBlock(60, 200); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(60, 120) .Content(page => { page.Mock("a").Position(0, 0).Size(60, 120); }); document .Page() .RequiredAreaSize(60, 80) .Content(page => { page.Mock("a").Position(0, 0).Size(60, 80); }); }); } [Test] public void Vertical() { LayoutTest .HavingSpaceOfSize(100, 120) .ForContent(content => { content .ShrinkVertical() .Mock("a").ContinuousBlock(60, 200); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(60, 120) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 120); }); document .Page() .RequiredAreaSize(60, 80) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 80); }); }); } [Test] public void Horizontal() { LayoutTest .HavingSpaceOfSize(100, 120) .ForContent(content => { content .ShrinkHorizontal() .Mock("a").ContinuousBlock(60, 200); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(60, 120) .Content(page => { page.Mock("a").Position(0, 0).Size(60, 120); }); document .Page() .RequiredAreaSize(60, 80) .Content(page => { page.Mock("a").Position(0, 0).Size(60, 120); }); }); } [Test] public void ContentFromRightToLeft() { LayoutTest .HavingSpaceOfSize(100, 120) .ForContent(content => { content .ContentFromRightToLeft() .Shrink() .Mock("a").ContinuousBlock(60, 200); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(60, 120) .Content(page => { page.Mock("a").Position(40, 0).Size(60, 120); }); document .Page() .RequiredAreaSize(60, 80) .Content(page => { page.Mock("a").Position(40, 0).Size(60, 80); }); }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/SimpleRotateTests.cs ================================================ using QuestPDF.Helpers; namespace QuestPDF.LayoutTests; public class SimpleRotateTests { private void DrawTestSubject(IContainer container) { container .Column(column => { column.Item().Row(row => { row.AutoItem().Mock("a").SolidBlock(100, 100); row.AutoItem().Mock("b").SolidBlock(100, 100); row.AutoItem().Mock("c").SolidBlock(100, 100); }); column.Item().Row(row => { row.AutoItem().Mock("d").SolidBlock(100, 100); row.AutoItem().Mock("e").SolidBlock(100, 100); row.AutoItem().Mock("f").SolidBlock(100, 100); }); }); } #region Single Page [Test] public void NoRotation() { LayoutTest .HavingSpaceOfSize(500, 500) .ForContent(content => { content.Shrink().Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(300, 200) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(100, 0).Size(100, 100); page.Mock("c").Position(200, 0).Size(100, 100); page.Mock("d").Position(0, 100).Size(100, 100); page.Mock("e").Position(100, 100).Size(100, 100); page.Mock("f").Position(200, 100).Size(100, 100); }); }); } [Test] public void OneRotation() { LayoutTest .HavingSpaceOfSize(500, 500) .ForContent(content => { content .Shrink() .RotateRight() // <- .Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(200, 300) .Content(page => { page.Mock("a").Position(200, 0).Size(100, 100); page.Mock("b").Position(200, 100).Size(100, 100); page.Mock("c").Position(200, 200).Size(100, 100); page.Mock("d").Position(100, 0).Size(100, 100); page.Mock("e").Position(100, 100).Size(100, 100); page.Mock("f").Position(100, 200).Size(100, 100); }); }); } [Test] public void TwoRotations() { LayoutTest .HavingSpaceOfSize(500, 500) .ForContent(content => { content .Shrink() .RotateRight() // <- .RotateRight() .Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(300, 200) .Content(page => { page.Mock("a").Position(300, 200).Size(100, 100); page.Mock("b").Position(200, 200).Size(100, 100); page.Mock("c").Position(100, 200).Size(100, 100); page.Mock("d").Position(300, 100).Size(100, 100); page.Mock("e").Position(200, 100).Size(100, 100); page.Mock("f").Position(100, 100).Size(100, 100); }); }); } [Test] public void ThreeRotation() { LayoutTest .HavingSpaceOfSize(500, 500) .ForContent(content => { content .Shrink() .RotateRight() // <- .RotateRight() .RotateRight() .Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(200, 300) .Content(page => { page.Mock("a").Position(0, 300).Size(100, 100); page.Mock("b").Position(0, 200).Size(100, 100); page.Mock("c").Position(0, 100).Size(100, 100); page.Mock("d").Position(100, 300).Size(100, 100); page.Mock("e").Position(100, 200).Size(100, 100); page.Mock("f").Position(100, 100).Size(100, 100); }); }); } #endregion #region Paging [Test] public void NoRotationWithPaging() { LayoutTest .HavingSpaceOfSize(500, 150) .ForContent(content => { content.Shrink().Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(300, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 100); page.Mock("b").Position(100, 0).Size(100, 100); page.Mock("c").Position(200, 0).Size(100, 100); }); document .Page() .RequiredAreaSize(300, 100) .Content(page => { page.Mock("d").Position(0, 0).Size(100, 100); page.Mock("e").Position(100, 0).Size(100, 100); page.Mock("f").Position(200, 0).Size(100, 100); }); }); } [Test] public void OneRotationWithPaging() { LayoutTest .HavingSpaceOfSize(150, 500) .ForContent(content => { content .Shrink() .RotateRight() // <- .Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 300) .Content(page => { page.Mock("a").Position(100, 0).Size(100, 100); page.Mock("b").Position(100, 100).Size(100, 100); page.Mock("c").Position(100, 200).Size(100, 100); }); document .Page() .RequiredAreaSize(100, 300) .Content(page => { page.Mock("d").Position(100, 0).Size(100, 100); page.Mock("e").Position(100, 100).Size(100, 100); page.Mock("f").Position(100, 200).Size(100, 100); }); }); } [Test] public void TwoRotationsWithPaging() { LayoutTest .HavingSpaceOfSize(500, 150) .ForContent(content => { content .Shrink() .RotateRight() // <- .RotateRight() .Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(300, 100) .Content(page => { page.Mock("a").Position(300, 100).Size(100, 100); page.Mock("b").Position(200, 100).Size(100, 100); page.Mock("c").Position(100, 100).Size(100, 100); }); document .Page() .RequiredAreaSize(300, 100) .Content(page => { page.Mock("d").Position(300, 100).Size(100, 100); page.Mock("e").Position(200, 100).Size(100, 100); page.Mock("f").Position(100, 100).Size(100, 100); }); }); } [Test] public void ThreeRotationWithPaging() { LayoutTest .HavingSpaceOfSize(150, 500) .ForContent(content => { content .Shrink() .RotateRight() // <- .RotateRight() .RotateRight() .Element(DrawTestSubject); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(100, 300) .Content(page => { page.Mock("a").Position(0, 300).Size(100, 100); page.Mock("b").Position(0, 200).Size(100, 100); page.Mock("c").Position(0, 100).Size(100, 100); }); document .Page() .RequiredAreaSize(100, 300) .Content(page => { page.Mock("d").Position(0, 300).Size(100, 100); page.Mock("e").Position(0, 200).Size(100, 100); page.Mock("f").Position(0, 100).Size(100, 100); }); }); } #endregion } ================================================ FILE: Source/QuestPDF.LayoutTests/StopPagingTests.cs ================================================ namespace QuestPDF.LayoutTests; public class StopPagingTests { [Test] public void ChildReturnsWrap() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink() .Mock("a") .StopPaging() // <- .Mock("b") .SolidBlock(200, 200); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(0, 0) .Content(page => { page.Mock("a").Position(0, 0).Size(0, 0); }); }); } [Test] public void ChildReturnsPartialRender() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink() .Mock("a") .StopPaging() // <- .Mock("b") .ContinuousBlock(50, 150); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(50, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(50, 100); page.Mock("b").Position(0, 0).Size(50, 100); }); // remaining item space is ignored }); } [Test] public void ChildReturnsFullRender() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink() .Mock("a") .StopPaging() // <- .Mock("b") .SolidBlock(50, 50); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(50, 50) .Content(page => { page.Mock("a").Position(0, 0).Size(50, 50); page.Mock("b").Position(0, 0).Size(50, 50); }); }); } [Test] public void ChildReturnsEmpty() { LayoutTest .HavingSpaceOfSize(100, 100) .ForContent(content => { content.Shrink() .Mock("a") .StopPaging() // <- .Mock("b"); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(0, 0) .Content(page => { page.Mock("a").Position(0, 0).Size(0, 0); page.Mock("b").Position(0, 0).Size(0, 0); }); }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/TableTests.cs ================================================ namespace QuestPDF.LayoutTests; public class TableTests { [Test] public void RowSpan_CornerCase1() { LayoutTest .HavingSpaceOfSize(200, 400) .ForContent(content => { content .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); }); table.Cell() .RowSpan(2) .Mock("a") .SolidBlock(100, 100); table.Cell() .Mock("b") .SolidBlock(100, 350); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(200, 100) .Content(page => { page.Mock("a").Position(0, 0).Size(200, 100); }); document .Page() .RequiredAreaSize(200, 350) .Content(page => { page.Mock("b").Position(0, 0).Size(200, 350); }); }); } [Test] public void RowSpan_CornerCase2() { LayoutTest .HavingSpaceOfSize(200, 400) .ForContent(content => { content .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); }); table.Cell() .Column(1) .Row(1) .RowSpan(3) .Mock("a") .SolidBlock(100, 100); table.Cell() .Column(2) .Row(2) .Mock("b") .ContinuousBlock(100, 600); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(200, 400) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 400); page.Mock("b").Position(100, 0).Size(100, 400); }); document .Page() .RequiredAreaSize(200, 200) .Content(page => { page.Mock("a").Position(0, 0).Size(100, 200); page.Mock("b").Position(100, 0).Size(100, 200); }); }); } [Test] public void RowSpan_CornerCase3() { LayoutTest .HavingSpaceOfSize(200, 400) .ForContent(content => { content .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); }); table.Cell() .Column(1) .Row(1) .RowSpan(3) .Column(column => { foreach (var i in Enumerable.Range(1, 5)) { column.Item().Width(200).Height(200).Mock($"{i}"); } }); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(200, 400) .Content(page => { page.Mock("1").Position(0, 0).Size(200, 200); page.Mock("2").Position(0, 200).Size(200, 200); }); document .Page() .RequiredAreaSize(200, 400) .Content(page => { page.Mock("3").Position(0, 0).Size(200, 200); page.Mock("4").Position(0, 200).Size(200, 200); }); document .Page() .RequiredAreaSize(200, 200) .Content(page => { page.Mock("5").Position(0, 0).Size(200, 200); }); }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/TestEngine/ContinuousBlock.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Helpers; using QuestPDF.Skia; namespace QuestPDF.LayoutTests.TestEngine; internal class ContinuousBlock : Element, IStateful { public float TotalWidth { get; set; } public float TotalHeight { get; set; } internal override SpacePlan Measure(Size availableSpace) { if (TotalWidth > availableSpace.Width + Size.Epsilon) return SpacePlan.Wrap("The content requires more horizontal space than available."); if (availableSpace.Height < Size.Epsilon) return SpacePlan.Wrap("The content requires more vertical space than available."); var remainingHeight = TotalHeight - HeightOffset; if (remainingHeight < Size.Epsilon) return SpacePlan.FullRender(Size.Zero); if (remainingHeight > availableSpace.Height) return SpacePlan.PartialRender(TotalWidth, availableSpace.Height); return SpacePlan.FullRender(TotalWidth, remainingHeight); } internal override void Draw(Size availableSpace) { var height = Math.Min(TotalHeight - HeightOffset, availableSpace.Height); var size = new Size(TotalWidth, height); HeightOffset += height; using var paint = new SkPaint(); paint.SetSolidColor(Colors.Grey.Medium); Canvas.DrawRectangle(Position.Zero, size, paint); if (HeightOffset > TotalHeight - Size.Epsilon) HeightOffset = 0; } #region IStateful private float HeightOffset { get; set; } public void ResetState(bool hardReset = false) => HeightOffset = 0; public object GetState() => HeightOffset; public void SetState(object state) => HeightOffset = (float) state; #endregion } ================================================ FILE: Source/QuestPDF.LayoutTests/TestEngine/DrawingRecorder.cs ================================================ using System.Collections.ObjectModel; namespace QuestPDF.LayoutTests.TestEngine; internal class ElementDrawingEvent { public string ObserverId { get; set; } public int PageNumber { get; set; } public Position Position { get; set; } public Size Size { get; set; } public object? StateAfterDrawing { get; set; } } internal class DrawingRecorder { private List DrawingEvents { get; } = []; public void Record(ElementDrawingEvent elementDrawingEvent) { DrawingEvents.Add(elementDrawingEvent); } public IReadOnlyCollection GetDrawingEvents() { return new ReadOnlyCollection(DrawingEvents); } } ================================================ FILE: Source/QuestPDF.LayoutTests/TestEngine/ElementObserver.cs ================================================ using System.Diagnostics; using QuestPDF.Drawing.DrawingCanvases; using QuestPDF.Drawing.Proxy; using QuestPDF.Elements; using QuestPDF.Helpers; namespace QuestPDF.LayoutTests.TestEngine; internal class ElementObserver : ContainerElement { public string? ObserverId { get; set; } public DrawingRecorder? DrawingRecorder { get; set; } internal override void Draw(Size availableSpace) { Debug.Assert(ObserverId != null); Debug.Assert(DrawingRecorder != null); var matrix = Canvas.GetCurrentMatrix(); var drawingEvent = new ElementDrawingEvent { ObserverId = ObserverId, PageNumber = PageContext.CurrentPage, Position = new Position(matrix.TranslateX, matrix.TranslateY), Size = ObserverId == "$document" ? Child.Measure(availableSpace) : availableSpace }; if (!Canvas.Is()) DrawingRecorder?.Record(drawingEvent); var matrixBeforeDraw = Canvas.GetCurrentMatrix().ToMatrix4x4(); base.Draw(availableSpace); var matrixAfterDraw = Canvas.GetCurrentMatrix().ToMatrix4x4(); if (matrixAfterDraw != matrixBeforeDraw) throw new InvalidOperationException("Canvas state was not restored after drawing operation."); drawingEvent.StateAfterDrawing = (GetRealChild() as IStateful)?.GetState(); } private Element GetRealChild() { var result = Child; while (true) { if (result is ElementProxy proxy) { result = proxy.Child; continue; } if (result is DebugPointer debugPointer) { result = debugPointer.Child; continue; } break; } return result; } } ================================================ FILE: Source/QuestPDF.LayoutTests/TestEngine/ElementObserverSetter.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Helpers; namespace QuestPDF.LayoutTests.TestEngine; internal class ElementObserverSetter : ContainerElement { public required DrawingRecorder Recorder { get; init; } internal override SpacePlan Measure(Size availableSpace) { SetRecorderOnChildren(); return base.Measure(availableSpace); } internal override void Draw(Size availableSpace) { SetRecorderOnChildren(); base.Draw(availableSpace); } private void SetRecorderOnChildren() { this.VisitChildren(x => { if (x is ElementObserver observer) observer.DrawingRecorder = Recorder; }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/TestEngine/FluentExtensions.cs ================================================ using QuestPDF.Helpers; namespace QuestPDF.LayoutTests.TestEngine; internal class ExpectedDocumentLayoutDescriptor(DrawingRecorder DrawingRecorder) { private int CurrentPage { get; set; } = 1; public ExpectedPageLayoutDescriptor Page() { return new ExpectedPageLayoutDescriptor(DrawingRecorder, CurrentPage++); } } internal class ExpectedPageLayoutDescriptor(DrawingRecorder DrawingRecorder, int CurrentPageNumber) { public ExpectedPageLayoutDescriptor RequiredAreaSize(float width, float height) { DrawingRecorder.Record(new ElementDrawingEvent { ObserverId = "$document", PageNumber = CurrentPageNumber, Size = new Size(width, height) }); return this; } public void Content(Action content) { var pageContent = new ExpectedPageContentDescriptor(DrawingRecorder, CurrentPageNumber); content(pageContent); } } internal class ExpectedPageContentDescriptor(DrawingRecorder drawingRecorder, int CurrentPageNumber) { public ExpectedMockPositionDescriptor Mock(string mockId) { var elementDrawingEvent = new ElementDrawingEvent { ObserverId = mockId, PageNumber = CurrentPageNumber, }; drawingRecorder.Record(elementDrawingEvent); return new ExpectedMockPositionDescriptor(elementDrawingEvent); } } internal class ExpectedMockPositionDescriptor(ElementDrawingEvent drawingEvent) { public ExpectedMockPositionDescriptor Position(float x, float y) { drawingEvent.Position = new Position(x, y); return this; } public ExpectedMockPositionDescriptor Size(float width, float height) { drawingEvent.Size = new Size(width, height); return this; } public ExpectedMockPositionDescriptor State(object state) { drawingEvent.StateAfterDrawing = state; return this; } } internal static class FluentExtensions { public const string DefaultMockId = "$mock"; public static IContainer Mock(this IContainer element, string id) { return element.Element(new ElementObserver { ObserverId = id }); } public static IContainer ElementObserverSetter(this IContainer element, DrawingRecorder recorder) { return element.Element(new ElementObserverSetter { Recorder = recorder }); } public static IContainer Size(this IContainer element, float width, float height) { return element.Width(width).Height(height); } public static void ContinuousBlock(this IContainer element, float width = 1f, float height = 1f) { element.Element(new ContinuousBlock { TotalWidth = width, TotalHeight = height }); } public static void SolidBlock(this IContainer element, float width = 1f, float height = 1f) { element.Element(new SolidBlock { TotalWidth = width, TotalHeight = height }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/TestEngine/LayoutTest.cs ================================================ using System.Runtime.CompilerServices; using System.Text; using NUnit.Framework.Constraints; using QuestPDF.Drawing.DocumentCanvases; using QuestPDF.Drawing.Exceptions; using QuestPDF.Elements; using QuestPDF.Helpers; namespace QuestPDF.LayoutTests.TestEngine; internal class LayoutTest { private string TestIdentifier { get; set; } private Size AvailableSpace { get; set; } private DrawingRecorder ActualDrawingRecorder { get; } = new(); private DrawingRecorder ExpectedDrawingRecorder { get; } = new(); private IContainer? Content { get; set; } private static readonly NUnitEqualityComparer Comparer = new(); public static LayoutTest HavingSpaceOfSize(float width, float height, [CallerMemberName] string testIdentifier = "test") { var layoutTest = new LayoutTest { TestIdentifier = testIdentifier, AvailableSpace = new Size(width, height) }; return layoutTest; } public LayoutTest ForContent(Action handler) { if (Content != null) throw new InvalidOperationException("Content has already been defined."); Content = new Container(); Content .Width(AvailableSpace.Width) .Height(AvailableSpace.Height) .ElementObserverSetter(ActualDrawingRecorder) .Mock("$document") .Element(handler); return this; } public void ExpectDrawResult(Action handler) { if (!ActualDrawingRecorder.GetDrawingEvents().Any()) PerformTest(); var builder = new ExpectedDocumentLayoutDescriptor(ExpectedDrawingRecorder); handler(builder); var actualDrawingEvents = ActualDrawingRecorder.GetDrawingEvents(); var expectedDrawingEvents = ExpectedDrawingRecorder.GetDrawingEvents(); if (CheckIfIdentical(actualDrawingEvents, expectedDrawingEvents)) { Assert.Pass(); } else { DrawLog(actualDrawingEvents, expectedDrawingEvents); Assert.Fail($"The drawing operations do not match the expected result. See the log above for details. Test identifier: '{TestIdentifier}'."); } static bool CheckIfIdentical(IReadOnlyCollection actual, IReadOnlyCollection expected) { if (actual.Count != expected.Count) return false; return actual.Zip(expected, Compare).All(x => x); } static bool Compare(ElementDrawingEvent? actual, ElementDrawingEvent? expected) { if (actual == null && expected == null) return true; if (actual == null || expected == null) return false; var tolerance = Tolerance.Default; return actual.ObserverId == expected.ObserverId && actual.PageNumber == expected.PageNumber && Position.Equal(actual.Position, expected.Position) && Size.Equal(actual.Size, expected.Size) && (expected.StateAfterDrawing == null || Comparer.AreEqual(actual.StateAfterDrawing, expected.StateAfterDrawing, ref tolerance)); } static void DrawLog(IReadOnlyCollection actualEvents, IReadOnlyCollection expectedEvents) { var identicalLines = actualEvents.Zip(expectedEvents, Compare).TakeWhile(x => x).Count(); if (identicalLines > 0) { TestContext.Out.WriteLine("IDENTICAL"); TestContext.Out.WriteLine(DrawHeader()); foreach (var actualEvent in actualEvents.Take(identicalLines)) TestContext.Out.WriteLine($"🟩\t{GetEventAsText(actualEvent)}"); } if (expectedEvents.Count > identicalLines) { TestContext.Out.WriteLine(); TestContext.Out.WriteLine("EXPECTED"); TestContext.Out.WriteLine(DrawHeader()); foreach (var expectedEvent in expectedEvents.Skip(identicalLines)) TestContext.Out.WriteLine($"🟧\t{GetEventAsText(expectedEvent)}"); } if (actualEvents.Count > identicalLines) { TestContext.Out.WriteLine(); TestContext.Out.WriteLine("ACTUAL"); TestContext.Out.WriteLine(DrawHeader()); foreach (var actualEvent in actualEvents.Skip(identicalLines)) TestContext.Out.WriteLine($"🟥\t{GetEventAsText(actualEvent)}"); } } static string DrawHeader() { var mock = "Mock".PadRight(12); var page = "Page".PadRight(6); var x = "X".PadRight(8); var y = "Y".PadRight(8); var width = "W".PadRight(10); var height = "H"; return $"\t{mock} {page} {x} {y} {width} {height}"; } static string GetEventAsText(ElementDrawingEvent drawingEvent) { var observerId = drawingEvent.ObserverId.PadRight(12); var pageNumber = $"{drawingEvent.PageNumber}".PadRight(6); var positionX = $"{drawingEvent.Position.X}".PadRight(8); var positionY = $"{drawingEvent.Position.Y}".PadRight(8); var sizeWidth = $"{drawingEvent.Size.Width}".PadRight(10); var sizeHeight = $"{drawingEvent.Size.Height}"; return $"{observerId} {pageNumber} {positionX} {positionY} {sizeWidth} {sizeHeight}"; } } public void ExpectLayoutException(string reason) { try { QuestPDF.Settings.EnableDebugging = true; PerformTest(); } catch (DocumentLayoutException e) { Assert.That(e.Message.Contains(reason)); Assert.Pass($"The expected exception was thrown: {e.Message}"); } catch { Assert.Fail("Un expected exception was thrown."); } } private void PerformTest() { Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(Size.Infinity, Size.Infinity)); page.Content().Element(Content); }); }) .Generate(new DiscardDocumentCanvas()); } public LayoutTest VisualizeOutput() { if (Content == null) throw new InvalidOperationException("Content has not been defined."); Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(Size.Infinity, Size.Infinity)); page.Content().Element(Content); }); }) .GeneratePdfAndShow(); return this; } } ================================================ FILE: Source/QuestPDF.LayoutTests/TestEngine/SolidBlock.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Helpers; using QuestPDF.Skia; namespace QuestPDF.LayoutTests.TestEngine; internal class SolidBlock : Element, IStateful { public float TotalWidth { get; set; } public float TotalHeight { get; set; } internal override SpacePlan Measure(Size availableSpace) { if (IsRendered) return SpacePlan.Empty(); if (TotalWidth > availableSpace.Width + Size.Epsilon) return SpacePlan.Wrap("The content requires more horizontal space than available."); if (TotalHeight > availableSpace.Height + Size.Epsilon) return SpacePlan.Wrap("The content requires more vertical space than available."); return SpacePlan.FullRender(TotalWidth, TotalHeight); } internal override void Draw(Size availableSpace) { using var paint = new SkPaint(); paint.SetSolidColor(Placeholders.BackgroundColor()); Canvas.DrawRectangle(Position.Zero, availableSpace, paint); 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.LayoutTests/TranslateTests.cs ================================================ namespace QuestPDF.LayoutTests; public class TranslateTests { [Test] public void HorizontalTranslation() { LayoutTest .HavingSpaceOfSize(100, 120) .ForContent(content => { content.Shrink().TranslateX(15).Mock("a").SolidBlock(40, 50); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(40, 50) .Content(page => { page.Mock("a").Position(15, 0).Size(40, 50); }); }); } [Test] public void VerticalTranslation() { LayoutTest .HavingSpaceOfSize(100, 120) .ForContent(content => { content.Shrink().TranslateY(25).Mock("a").SolidBlock(30, 40); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(30, 40) .Content(page => { page.Mock("a").Position(0, 25).Size(30, 40); }); }); } [Test] public void MultipleItemsWithTranslation() { LayoutTest .HavingSpaceOfSize(100, 120) .ForContent(content => { content.Shrink().Column(column => { column.Item().TranslateX(5).TranslateY(10).Mock("a").SolidBlock(40, 20); column.Item().TranslateX(-10).TranslateY(20).Mock("b").SolidBlock(30, 25); column.Item().TranslateX(30).TranslateY(-15).Mock("c").SolidBlock(50, 15); }); }) .ExpectDrawResult(document => { document .Page() .RequiredAreaSize(50, 60) .Content(page => { page.Mock("a").Position(5, 10).Size(50, 20); page.Mock("b").Position(-10, 40).Size(50, 25); page.Mock("c").Position(30, 30).Size(50, 15); }); }); } } ================================================ FILE: Source/QuestPDF.LayoutTests/Usings.cs ================================================ global using NUnit.Framework; global using QuestPDF.Fluent; global using QuestPDF.Infrastructure; global using QuestPDF.LayoutTests.TestEngine; ================================================ FILE: Source/QuestPDF.ReportSample/DataSource.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using QuestPDF.Helpers; namespace QuestPDF.ReportSample { public static class DataSource { public static ReportModel GetReport() { return new ReportModel { Title = "Sample Report Document", HeaderFields = HeaderFields(), LogoData = Helpers.GetImage("Logo.png"), Sections = Enumerable.Range(0, 40).Select(x => GenerateSection()).ToList(), Photos = Enumerable.Range(0, 25).Select(x => GetReportPhotos()).ToList() }; List HeaderFields() { return new List { new ReportHeaderField() { Label = "Scope", Value = "Internal activities" }, new ReportHeaderField() { Label = "Author", Value = "Marcin Ziąbek" }, new ReportHeaderField() { Label = "Date", Value = DateTime.Now.ToString("g") }, new ReportHeaderField() { Label = "Status", Value = "Completed, found 2 issues" } }; } ReportSection GenerateSection() { var sectionLength = Helpers.Random.NextDouble() > 0.75 ? Helpers.Random.Next(20, 40) : Helpers.Random.Next(5, 10); return new ReportSection { Title = Placeholders.Label(), Parts = Enumerable.Range(0, sectionLength).Select(x => GetRandomElement()).ToList() }; } ReportSectionElement GetRandomElement() { var random = Helpers.Random.NextDouble(); if (random < 0.9f) return GetTextElement(); if (random < 0.95f) return GetMapElement(); return GetPhotosElement(); } ReportSectionText GetTextElement() { return new ReportSectionText { Label = Placeholders.Label(), Text = Placeholders.Paragraph() }; } ReportSectionMap GetMapElement() { return new ReportSectionMap { Label = "Location", Location = Helpers.RandomLocation() }; } ReportSectionPhotos GetPhotosElement() { return new ReportSectionPhotos { Label = "Photos", PhotoCount = Helpers.Random.Next(1, 15) }; } ReportPhoto GetReportPhotos() { return new ReportPhoto() { Comments = Placeholders.Sentence(), Date = DateTime.Now - TimeSpan.FromDays(Helpers.Random.NextDouble() * 100), Location = Helpers.RandomLocation() }; } } } } ================================================ FILE: Source/QuestPDF.ReportSample/Helpers.cs ================================================ using System; using System.Collections.Concurrent; using System.IO; using System.Text; using QuestPDF.Fluent; using QuestPDF.Infrastructure; using SkiaSharp; namespace QuestPDF.ReportSample { public static class Helpers { public static Random Random { get; } = new Random(); public static string GetTestItem(string path) => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", path); public static byte[] GetImage(string name) { var photoPath = GetTestItem(name); return SKImage.FromEncodedData(photoPath).EncodedData.ToArray(); } public static Location RandomLocation() { return new Location { Longitude = Helpers.Random.NextDouble() * 360f - 180f, Latitude = Helpers.Random.NextDouble() * 180f - 90f }; } private static readonly ConcurrentDictionary RomanNumeralCache = new ConcurrentDictionary(); public static string FormatAsRomanNumeral(this int number) { if (number < 0 || number > 3999) throw new ArgumentOutOfRangeException(nameof(number), "Number should be in range from 1 to 3999"); return RomanNumeralCache.GetOrAdd(number, x => { if (x >= 1000) return "M" + FormatAsRomanNumeral(x - 1000); if (x >= 900) return "CM" + FormatAsRomanNumeral(x - 900); if (x >= 500) return "D" + FormatAsRomanNumeral(x - 500); if (x >= 400) return "CD" + FormatAsRomanNumeral(x - 400); if (x >= 100) return "C" + FormatAsRomanNumeral(x - 100); if (x >= 90) return "XC" + FormatAsRomanNumeral(x - 90); if (x >= 50) return "L" + FormatAsRomanNumeral(x - 50); if (x >= 40) return "XL" + FormatAsRomanNumeral(x - 40); if (x >= 10) return "X" + FormatAsRomanNumeral(x - 10); if (x >= 9) return "IX" + FormatAsRomanNumeral(x - 9); if (x >= 5) return "V" + FormatAsRomanNumeral(x - 5); if (x >= 4) return "IV" + FormatAsRomanNumeral(x - 4); if (x >= 1) return "I" + FormatAsRomanNumeral(x - 1); return string.Empty; }); } public static void SkiaSharpCanvas(this IContainer container, Action drawOnCanvas) { container.Svg(size => { using var stream = new MemoryStream(); using (var canvas = SKSvgCanvas.Create(new SKRect(0, 0, size.Width, size.Height), stream)) drawOnCanvas(canvas, size); var svgData = stream.ToArray(); return Encoding.UTF8.GetString(svgData); }); } public static void SkiaSharpRasterized(this IContainer container, Action drawOnCanvas) { container.Image(payload => { using var bitmap = new SKBitmap(payload.ImageSize.Width, payload.ImageSize.Height); using (var canvas = new SKCanvas(bitmap)) { var scalingFactor = payload.Dpi / (float)DocumentSettings.DefaultRasterDpi; canvas.Scale(scalingFactor); drawOnCanvas(canvas, payload.AvailableSpace); } return bitmap.Encode(SKEncodedImageFormat.Png, 100).ToArray(); }); } } } ================================================ FILE: Source/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample.Layouts { public class DifferentHeadersTemplate : IDocument { public DocumentMetadata GetMetadata() => DocumentMetadata.Default; public DocumentSettings GetSettings() => DocumentSettings.Default; public void Compose(IDocumentContainer container) { container .Page(page => { page.Margin(40); page.Size(PageSizes.A4); page.Header().Element(ComposeHeader); page.Content().Element(ComposeContent); page.Footer().Element(ComposeFooter); }); } private void ComposeHeader(IContainer container) { container.Background(Colors.Grey.Lighten3).Border(1).Column(column => { column.Item().ShowOnce().Padding(5).AlignMiddle().Row(row => { row.RelativeItem(2).AlignMiddle().Text("PRIMARY HEADER").FontColor(Colors.Grey.Darken3).FontSize(30).Bold(); row.RelativeItem(1).AlignRight().MinimalBox().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30); }); column.Item().SkipOnce().Padding(5).Row(row => { row.RelativeItem(2).Text("SECONDARY HEADER").FontColor(Colors.Grey.Darken3).FontSize(30).Bold(); row.RelativeItem(1).AlignRight().MinimalBox().Background(Colors.Blue.Lighten4).Padding(15); }); }); } private void ComposeContent(IContainer container) { container.Column(column => { column.Item().PaddingVertical(80).Text("First"); column.Item().PageBreak(); column.Item().PaddingVertical(80).Text("Second"); column.Item().PageBreak(); column.Item().PaddingVertical(80).Text("Third"); column.Item().PageBreak(); }); } private void ComposeFooter(IContainer container) { container.Background(Colors.Grey.Lighten3).Column(column => { column.Item().ShowOnce().Background(Colors.Grey.Lighten3).Row(row => { row.RelativeItem().Text(x => { x.CurrentPageNumber(); x.Span(" / "); x.TotalPages(); }); row.RelativeItem().AlignRight().Text("Footer for header"); }); column.Item().SkipOnce().Background(Colors.Grey.Lighten3).Row(row => { row.RelativeItem().Text(x => { x.CurrentPageNumber(); x.Span(" / "); x.TotalPages(); }); row.RelativeItem().AlignRight().Text("Footer for every page except header"); }); }); } } } ================================================ FILE: Source/QuestPDF.ReportSample/Layouts/Helpers.cs ================================================ using System; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample.Layouts { public static class Helpers { static IContainer Cell(this IContainer container, bool background) { return container .Border(0.5f) .BorderColor(Colors.Grey.Lighten1) .Background(background ? Colors.Grey.Lighten4 : Colors.White) .Padding(5); } public static IContainer ValueCell(this IContainer container) { return container.Cell(false); } public static IContainer LabelCell(this IContainer container) { return container.Cell(true); } public static string Format(this Location location) { if (location == null) return string.Empty; var lon = location.Longitude; var lat = location.Latitude; var typeLon = lon > 0 ? "E" : "W"; lon = Math.Abs(lon); var typeLat = lat > 0 ? "N" : "S"; lat = Math.Abs(lat); return $"{lat:F5}° {typeLat} {lon:F5}° {typeLon}"; } } } ================================================ FILE: Source/QuestPDF.ReportSample/Layouts/ImagePlaceholder.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample.Layouts { public class ImagePlaceholder : IComponent { public static bool Solid { get; set; } = false; public void Compose(IContainer container) { if (Solid) container.Background(Placeholders.Color()); else container.Image(Placeholders.Image); } } } ================================================ FILE: Source/QuestPDF.ReportSample/Layouts/PhotoTemplate.cs ================================================ using System; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample.Layouts { public class PhotoTemplate : IComponent { public ReportPhoto Model { get; set; } public PhotoTemplate(ReportPhoto model) { Model = model; } public void Compose(IContainer container) { container .ShowEntire() .Column(column => { column.Spacing(5); column.Item().Element(PhotoWithMaps); column.Item().Element(PhotoDetails); }); } void PhotoWithMaps(IContainer container) { container .Row(row => { row.RelativeItem(2).AspectRatio(4 / 3f).Component(); row.RelativeItem().PaddingLeft(5).Column(column => { column.Spacing(7f); column.Item().AspectRatio(4 / 3f).Component(); column.Item().AspectRatio(4 / 3f).Component(); }); }); } void PhotoDetails(IContainer container) { container.Border(0.75f).BorderColor(Colors.Grey.Medium).Grid(grid => { grid.Columns(6); grid.Item().LabelCell().Text("Date"); grid.Item(2).ValueCell().Text(Model.Date?.ToString("g") ?? string.Empty); grid.Item().LabelCell().Text("Location"); grid.Item(2).ValueCell().Text(Model.Location.Format()); grid.Item().LabelCell().Text("Comments"); grid.Item(5).ValueCell().Text(Model.Comments); }); } } } ================================================ FILE: Source/QuestPDF.ReportSample/Layouts/SectionTemplate.cs ================================================ using System.Linq; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample.Layouts { public class SectionTemplate : IComponent { public ReportSection Model { get; set; } public SectionTemplate(ReportSection model) { Model = model; } public void Compose(IContainer container) { container .EnsureSpace() .Decoration(decoration => { decoration .Before() .PaddingBottom(5) .Text(Model.Title) .Style(Typography.Headline); decoration.Content().Border(0.75f).BorderColor(Colors.Grey.Medium).Column(column => { foreach (var part in Model.Parts) { column.Item().EnsureSpace(25).Row(row => { row.ConstantItem(150).LabelCell().Text(part.Label); var frame = row.RelativeItem().ValueCell(); if (part is ReportSectionText text) frame.ShowEntire().Text(text.Text); if (part is ReportSectionMap map) frame.Element(x => MapElement(x, map)); if (part is ReportSectionPhotos photos) frame.Element(x => PhotosElement(x, photos)); }); } }); }); } static void MapElement(IContainer container, ReportSectionMap model) { if (model.Location == null) { container.Text("No location provided"); return; } container.ShowEntire().Column(column => { column.Spacing(5); column.Item().MaxWidth(250).AspectRatio(4 / 3f).Component(); column.Item().Text(model.Location.Format()); }); } static void PhotosElement(IContainer container, ReportSectionPhotos model) { if (model.PhotoCount == 0) { container.Text("No photos").Style(Typography.Normal); return; } container.DebugArea("Photos").Grid(grid => { grid.Spacing(5); grid.Columns(3); Enumerable .Range(0, model.PhotoCount) .ToList() .ForEach(x => grid.Item().AspectRatio(4 / 3f).Component()); }); } } } ================================================ FILE: Source/QuestPDF.ReportSample/Layouts/StandardReport.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample.Layouts { public class StandardReport : IDocument { private ReportModel Model { get; } public StandardReport(ReportModel model) { Model = model; } public DocumentMetadata GetMetadata() { return new DocumentMetadata() { Title = Model.Title }; } public DocumentSettings GetSettings() => DocumentSettings.Default; public void Compose(IDocumentContainer container) { container .Page(page => { page.DefaultTextStyle(Typography.Normal); page.MarginVertical(40); page.MarginHorizontal(50); page.Size(PageSizes.A4); page.Header().Element(ComposeHeader); page.Content().Element(ComposeContent); page.Footer().AlignCenter().Text(text => { text.CurrentPageNumber().Format(x => x?.FormatAsRomanNumeral() ?? "-----"); text.Span(" / "); text.TotalPages().Format(x => x?.FormatAsRomanNumeral() ?? "-----"); }); }); } private void ComposeHeader(IContainer container) { container.Column(column => { column.Item().Row(row => { row.Spacing(50); row.RelativeItem().PaddingTop(-10).Text(Model.Title).Style(Typography.Title); row.ConstantItem(90).Hyperlink("https://www.questpdf.com").MaxHeight(30).Component(); }); column.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal(); column.Item().ShowOnce().Grid(grid => { grid.Columns(2); grid.Spacing(5); foreach (var field in Model.HeaderFields) { grid.Item().Text(text => { text.Span($"{field.Label}: ").SemiBold(); text.Span(field.Value); }); } }); }); } void ComposeContent(IContainer container) { container.PaddingVertical(20).Column(column => { column.Spacing(20); column.Item().Component(new TableOfContentsTemplate(Model.Sections)); column.Item().PageBreak(); foreach (var section in Model.Sections) column.Item().Section(section.Title).Component(new SectionTemplate(section)); column.Item().PageBreak(); column.Item().Section("Photos"); foreach (var photo in Model.Photos) column.Item().Component(new PhotoTemplate(photo)); }); } } } ================================================ FILE: Source/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs ================================================ using System.Collections.Generic; using System.Drawing; using System.IO; using System.Text; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using SkiaSharp; namespace QuestPDF.ReportSample.Layouts { public class TableOfContentsTemplate : IComponent { private List Sections { get; } public TableOfContentsTemplate(List sections) { Sections = sections; } public void Compose(IContainer container) { container .Decoration(decoration => { decoration .Before() .PaddingBottom(5) .Text("Table of contents") .Style(Typography.Headline); decoration.Content().Column(column => { column.Spacing(5); for (var i = 0; i < Sections.Count; i++) column.Item().Element(c => DrawLink(c, i+1, Sections[i].Title)); column.Item().Element(c => DrawLink(c, Sections.Count+1, "Photos")); }); }); } private static void DrawLink(IContainer container, int number, string locationName) { container .SectionLink(locationName) .Row(row => { row.ConstantItem(20).Text($"{number}."); row.AutoItem().Text(locationName); row.RelativeItem() .PaddingHorizontal(2) .AlignBottom() .Height(3) .SkiaSharpRasterized((canvas, size) => { using var paint = new SKPaint { StrokeWidth = 1, PathEffect = SKPathEffect.CreateDash(new float[] { 1, 3 }, 0), }; canvas.Translate(0, 1); canvas.DrawLine(0, 0, size.Width, 0, paint); }); row.AutoItem().Text(text => { text.BeginPageNumberOfSection(locationName); text.Span(" - "); text.EndPageNumberOfSection(locationName); var lengthStyle = TextStyle.Default.FontColor(Colors.Grey.Medium); text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => { var formatted = x == 1 ? "1 page long" : $"{x} pages long"; return $" ({formatted})"; }); }); }); } } } ================================================ FILE: Source/QuestPDF.ReportSample/Models.cs ================================================ using System; using System.Collections.Generic; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample { public class ReportModel { public string Title { get; set; } public byte[] LogoData { get; set; } public List HeaderFields { get; set; } public List Sections { get; set; } public List Photos { get; set; } } public class ReportHeaderField { public string Label { get; set; } public string Value { get; set; } } public class Location { public double Longitude { get; set; } public double Latitude { get; set; } } public class ReportSection { public string Title { get; set; } public List Parts { get; set; } } public abstract class ReportSectionElement { public string Label { get; set; } } public class ReportSectionText : ReportSectionElement { public string Text { get; set; } } public class ReportSectionMap : ReportSectionElement { public Location Location { get; set; } } public class ReportSectionPhotos : ReportSectionElement { public int PhotoCount { get; set; } } public class ReportPhoto { public Location Location { get; set; } public DateTime? Date { get; set; } public string Comments { get; set; } } } ================================================ FILE: Source/QuestPDF.ReportSample/QuestPDF.ReportSample.csproj ================================================  net10.0 en false QuestPDF.ReportSample PreserveNewest ================================================ FILE: Source/QuestPDF.ReportSample/Tests.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Infrastructure; using QuestPDF.ReportSample.Layouts; namespace QuestPDF.ReportSample { public class ReportGeneration { private StandardReport Report { get; set; } [SetUp] public void SetUp() { QuestPDF.Settings.License = LicenseType.Community; var model = DataSource.GetReport(); Report = new StandardReport(model); //ImagePlaceholder.Solid = true; } [Test] [Ignore("This test is for manual testing only.")] public void GeneratePdfAndShow() { Report.GeneratePdfAndShow(); } [Test] [Ignore("This test is for manual testing only.")] public void GenerateXpsAndShow() { Report.GenerateXpsAndShow(); } [Test] public void GeneratePdfForManualVerificationTesting() { Report.GeneratePdf("report.pdf"); } [Test] [Description("This test is important, as it checks if all IDisposables are properly disposed, and there are no memory leaks.")] public async Task CheckFinalizersStability() { Settings.EnableCaching = true; Report.GeneratePdf(); Report.GenerateImages(new ImageGenerationSettings { RasterDpi = 72 }); Report.GenerateSvg(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Report.GenerateXps(); await Task.Delay(1000); GC.Collect(); GC.WaitForPendingFinalizers(); await Task.Delay(1000); } } } ================================================ FILE: Source/QuestPDF.ReportSample/Typography.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample { public static class Typography { public static TextStyle Title => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Blue.Darken3).FontSize(26).Black(); public static TextStyle Headline => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Blue.Medium).FontSize(16).SemiBold(); public static TextStyle Normal => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Black).FontSize(10).LineHeight(1.2f); } } ================================================ FILE: Source/QuestPDF.UnitTests/AlignmentTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class AlignmentTests { [Test] public void Measure() => SimpleContainerTests.Measure(); [Test] public void Draw_HorizontalCenter_VerticalCenter() { TestPlan .For(x => new Alignment { Horizontal = HorizontalAlignment.Center, Vertical = VerticalAlignment.Middle, Child = x.CreateChild() }) .DrawElement(new Size(1000, 500)) .ExpectChildMeasure(expectedInput: new Size(1000, 500), returns: SpacePlan.PartialRender(new Size(400, 200))) .ExpectCanvasTranslate(new Position(300, 150)) .ExpectChildDraw(new Size(400, 200)) .ExpectCanvasTranslate(new Position(-300, -150)) .CheckDrawResult(); } [Test] public void Draw_HorizontalLeft_VerticalCenter() { TestPlan .For(x => new Alignment { Horizontal = HorizontalAlignment.Left, Vertical = VerticalAlignment.Middle, Child = x.CreateChild() }) .DrawElement(new Size(400, 300)) .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.FullRender(new Size(100, 50))) .ExpectCanvasTranslate(new Position(0, 125)) .ExpectChildDraw(new Size(100, 50)) .ExpectCanvasTranslate(new Position(0, -125)) .CheckDrawResult(); } [Test] public void Draw_HorizontalCenter_VerticalBottom() { TestPlan .For(x => new Alignment { Horizontal = HorizontalAlignment.Center, Vertical = VerticalAlignment.Bottom, Child = x.CreateChild() }) .DrawElement(new Size(400, 300)) .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.FullRender(new Size(100, 50))) .ExpectCanvasTranslate(new Position(150, 250)) .ExpectChildDraw(new Size(100, 50)) .ExpectCanvasTranslate(new Position(-150, -250)) .CheckDrawResult(); } [Test] public void Draw_HorizontalRight_VerticalTop() { TestPlan .For(x => new Alignment { Horizontal = HorizontalAlignment.Right, Vertical = VerticalAlignment.Top, Child = x.CreateChild() }) .DrawElement(new Size(400, 300)) .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.FullRender(new Size(100, 50))) .ExpectCanvasTranslate(new Position(300, 0)) .ExpectChildDraw(new Size(100, 50)) .ExpectCanvasTranslate(new Position(-300, 0)) .CheckDrawResult(); } [Test] public void Draw_HorizontalCenter_VerticalNone() { TestPlan .For(x => new Alignment { Horizontal = HorizontalAlignment.Center, Vertical = null, Child = x.CreateChild() }) .DrawElement(new Size(400, 300)) .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.FullRender(new Size(100, 50))) .ExpectCanvasTranslate(new Position(150, 0)) .ExpectChildDraw(new Size(100, 300)) .ExpectCanvasTranslate(new Position(-150, 0)) .CheckDrawResult(); } [Test] public void Draw_HorizontalNone_VerticalMiddle() { TestPlan .For(x => new Alignment { Horizontal = null, Vertical = VerticalAlignment.Middle, Child = x.CreateChild() }) .DrawElement(new Size(400, 300)) .ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.FullRender(new Size(100, 50))) .ExpectCanvasTranslate(new Position(0, 125)) .ExpectChildDraw(new Size(400, 50)) .ExpectCanvasTranslate(new Position(0, -125)) .CheckDrawResult(); } } } ================================================ FILE: Source/QuestPDF.UnitTests/AspectRatioTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class AspectRatioTests { [Test] public void Measure_FitWidth_EnoughSpace_FullRender() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f }) .MeasureElement(new Size(400, 201)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(100, 50)) .CheckMeasureResult(SpacePlan.FullRender(400, 200)); } [Test] public void Measure_FitWidth_EnoughSpace_PartialRender() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f }) .MeasureElement(new Size(400, 201)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.PartialRender(100, 50)) .CheckMeasureResult(SpacePlan.PartialRender(400, 200)); } [Test] public void Measure_FitWidth_EnoughSpace_Wrap() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f }) .MeasureElement(new Size(400, 201)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("Forwarded from child")); } [Test] public void Measure_FitWidth_EnoughSpace() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitWidth, Ratio = 2f }) .MeasureElement(new Size(400, 201)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(100, 50)) .CheckMeasureResult(SpacePlan.FullRender(400, 200)); } [Test] public void Measure_FitWidth_NotEnoughSpace() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitWidth, Ratio = 2f }) .MeasureElement(new Size(400, 199)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .CheckMeasureResult(SpacePlan.Wrap("To preserve the target aspect ratio, the content requires more vertical space than available.")); } [Test] public void Measure_FitHeight_EnoughSpace() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitHeight, Ratio = 2f }) .MeasureElement(new Size(401, 200)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(100, 50)) .CheckMeasureResult(SpacePlan.FullRender(400, 200)); } [Test] public void Measure_FitHeight_NotEnoughSpace() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitHeight, Ratio = 2f }) .MeasureElement(new Size(399, 200)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .CheckMeasureResult(SpacePlan.Wrap("To preserve the target aspect ratio, the content requires more horizontal space than available.")); } [Test] public void Measure_FitArea_ToWidth() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(100, 50)) .CheckMeasureResult(SpacePlan.FullRender(400, 200)); } [Test] public void Measure_FitArea_ToHeight() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f }) .MeasureElement(new Size(500, 200)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(100, 50)) .CheckMeasureResult(SpacePlan.FullRender(400, 200)); } [Test] public void DrawChild_PerWidth() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f }) .DrawElement(new Size(500, 200)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw(new Size(400, 200)) .ExpectCanvasTranslate(0, 0) .CheckDrawResult(); } [Test] public void DrawChild_PerHeight() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f }) .DrawElement(new Size(400, 300)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw(new Size(400, 200)) .ExpectCanvasTranslate(0, 0) .CheckDrawResult(); } [Test] public void DrawChild_PerWidth_RightToLeft() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f, ContentDirection = ContentDirection.RightToLeft }) .DrawElement(new Size(500, 200)) .ExpectCanvasTranslate(100, 0) .ExpectChildDraw(new Size(400, 200)) .ExpectCanvasTranslate(-100, 0) .CheckDrawResult(); } [Test] public void DrawChild_PerHeight_RightToLeft() { TestPlan .For(x => new AspectRatio { Child = x.CreateChild(), Option = AspectRatioOption.FitArea, Ratio = 2f, ContentDirection = ContentDirection.RightToLeft }) .DrawElement(new Size(400, 300)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw(new Size(400, 200)) .ExpectCanvasTranslate(0, 0) .CheckDrawResult(); } } } ================================================ FILE: Source/QuestPDF.UnitTests/ColumnTests.cs ================================================ using System.Linq; using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class ColumnTests { private Column CreateColumnWithTwoItems(TestPlan testPlan) { return new Column { Items = { testPlan.CreateChild("first"), testPlan.CreateChild("second") } }; } private Column CreateColumnWithTwoItemsWhereFirstIsFullyRendered(TestPlan testPlan) { var column = CreateColumnWithTwoItems(testPlan); column.CurrentRenderingIndex = 1; return column; } #region Measure [Test] public void Measure_ReturnsWrap_WhenFirstChildWraps() { TestPlan .For(CreateColumnWithTwoItems) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("The available space is not sufficient for even partially rendering a single item.")); } [Test] public void Measure_ReturnsPartialRender_WhenFirstChildReturnsPartialRender() { TestPlan .For(CreateColumnWithTwoItems) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.PartialRender(300, 200)) .CheckMeasureResult(SpacePlan.PartialRender(300, 200)); } [Test] public void Measure_ReturnsPartialRender_WhenSecondChildWraps() { TestPlan .For(CreateColumnWithTwoItems) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100)) .ExpectChildMeasure("second", new Size(400, 200), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.PartialRender(200, 100)); } [Test] public void Measure_ReturnsPartialRender_WhenSecondChildReturnsPartialRender() { TestPlan .For(CreateColumnWithTwoItems) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100)) .ExpectChildMeasure("second", new Size(400, 200), SpacePlan.PartialRender(300, 150)) .CheckMeasureResult(SpacePlan.PartialRender(300, 250)); } [Test] public void Measure_ReturnsFullRender_WhenSecondChildReturnsFullRender() { TestPlan .For(CreateColumnWithTwoItems) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100)) .ExpectChildMeasure("second", new Size(400, 200), SpacePlan.FullRender(100, 50)) .CheckMeasureResult(SpacePlan.FullRender(200, 150)); } #endregion #region Draw [Test] public void Draw_WhenFirstChildWraps() { TestPlan .For(CreateColumnWithTwoItems) .DrawElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.Wrap("Mock")) .CheckDrawResult(); } [Test] public void Draw_WhenFirstChildPartiallyRenders() { TestPlan .For(CreateColumnWithTwoItems) .DrawElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.PartialRender(200, 100)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw("first", new Size(400, 100)) .ExpectCanvasTranslate(0, 0) .CheckDrawResult(); } [Test] public void Draw_WhenFirstChildFullyRenders_AndSecondChildWraps() { TestPlan .For(CreateColumnWithTwoItems) .DrawElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100)) .ExpectChildMeasure("second", new Size(400, 200), SpacePlan.Wrap("Mock")) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw("first", new Size(400, 100)) .ExpectCanvasTranslate(0, 0) .CheckDrawResult(); } [Test] public void Draw_WhenFirstChildFullyRenders_AndSecondChildPartiallyRenders() { TestPlan .For(CreateColumnWithTwoItems) .DrawElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100)) .ExpectChildMeasure("second", new Size(400, 200), SpacePlan.PartialRender(250, 150)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw("first", new Size(400, 100)) .ExpectCanvasTranslate(0, 0) .ExpectCanvasTranslate(0, 100) .ExpectChildDraw("second", new Size(400, 150)) .ExpectCanvasTranslate(0, -100) .CheckDrawResult(); } [Test] public void Draw_WhenFirstChildFullyRenders_AndSecondChildFullyRenders() { TestPlan .For(CreateColumnWithTwoItems) .DrawElement(new Size(400, 300)) .ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100)) .ExpectChildMeasure("second", new Size(400, 200), SpacePlan.FullRender(250, 150)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw("first", new Size(400, 100)) .ExpectCanvasTranslate(0, 0) .ExpectCanvasTranslate(0, 100) .ExpectChildDraw("second", new Size(400, 150)) .ExpectCanvasTranslate(0, -100) .CheckDrawResult(); } [Test] public void Draw_UsesEmpty_WhenFirstChildIsRendered() { TestPlan .For(CreateColumnWithTwoItemsWhereFirstIsFullyRendered) .DrawElement(new Size(400, 300)) .ExpectChildMeasure("second", new Size(400, 300), SpacePlan.PartialRender(200, 300)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw("second", new Size(400, 300)) .ExpectCanvasTranslate(0, 0) .CheckState(x => x.CurrentRenderingIndex > 0) .CheckDrawResult(); } [Test] public void Draw_DoesNotToggleFirstRenderedFlag_WhenSecondFullyRenders() { TestPlan .For(CreateColumnWithTwoItemsWhereFirstIsFullyRendered) .DrawElement(new Size(400, 300)) .ExpectChildMeasure("second", new Size(400, 300), SpacePlan.FullRender(200, 300)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw("second", new Size(400, 300)) .ExpectCanvasTranslate(0, 0) .CheckDrawResult() .CheckState(x => x.CurrentRenderingIndex == 2); } #endregion } } ================================================ FILE: Source/QuestPDF.UnitTests/ConstrainedTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class ConstrainedTests { #region Height [Test] public void Measure_MinHeight_ExpectWrap() { TestPlan .For(x => new Constrained { MinHeight = 100 }) .MeasureElement(new Size(400, 50)) .CheckMeasureResult(SpacePlan.Wrap("The available vertical space is less than the minimum height.")); } [Test] public void Measure_MinHeight_ExtendHeight() { TestPlan .For(x => new Constrained { MinHeight = 100, Child = x.CreateChild() }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(400, 50)) .CheckMeasureResult(SpacePlan.FullRender(400, 100)); } [Test] public void Measure_MinHeight_PassHeight() { TestPlan .For(x => new Constrained { MinHeight = 100, Child = x.CreateChild() }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(400, 150)) .CheckMeasureResult(SpacePlan.FullRender(400, 150)); } [Test] public void Measure_MaxHeight_Empty() { TestPlan .For(x => new Constrained { MaxHeight = 100 }) .MeasureElement(new Size(400, 150)) .CheckMeasureResult(SpacePlan.FullRender(0, 0)); } [Test] public void Measure_MaxHeight_PartialRender() { TestPlan .For(x => new Constrained { MaxHeight = 100, Child = x.CreateChild() }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 100), SpacePlan.PartialRender(400, 75)) .CheckMeasureResult(SpacePlan.PartialRender(400, 75)); } [Test] public void Measure_MaxHeight_ExpectWrap() { TestPlan .For(x => new Constrained { MaxHeight = 100, Child = x.CreateChild() }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(400, 100), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("Forwarded from child")); } #endregion #region Width [Test] public void Measure_MinWidth_ExpectWrap() { TestPlan .For(x => new Constrained { MinWidth = 100 }) .MeasureElement(new Size(50, 400)) .CheckMeasureResult(SpacePlan.Wrap("The available horizontal space is less than the minimum width.")); } [Test] public void Measure_MinWidth_ExtendHeight() { TestPlan .For(x => new Constrained { MinWidth = 100, Child = x.CreateChild() }) .MeasureElement(new Size(200, 400)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(200, 400), SpacePlan.FullRender(50, 400)) .CheckMeasureResult(SpacePlan.FullRender(100, 400)); } [Test] public void Measure_MinWidth_PassHeight() { TestPlan .For(x => new Constrained { MinWidth = 100, Child = x.CreateChild() }) .MeasureElement(new Size(200, 400)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(200, 400), SpacePlan.FullRender(150, 400)) .CheckMeasureResult(SpacePlan.FullRender(150, 400)); } [Test] public void Measure_MaxWidth_Empty() { TestPlan .For(x => new Constrained { MaxWidth = 100 }) .MeasureElement(new Size(150, 400)) .CheckMeasureResult(SpacePlan.FullRender(0, 0)); } [Test] public void Measure_MaxWidth_PartialRender() { TestPlan .For(x => new Constrained { MaxWidth = 100, Child = x.CreateChild() }) .MeasureElement(new Size(200, 400)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(100, 400), SpacePlan.PartialRender(75, 400)) .CheckMeasureResult(SpacePlan.PartialRender(75, 400)); } [Test] public void Measure_MaxWidth_ExpectWrap() { TestPlan .For(x => new Constrained { MaxWidth = 100, Child = x.CreateChild() }) .MeasureElement(new Size(200, 400)) .ExpectChildMeasure(Size.Zero, SpacePlan.PartialRender(Size.Zero)) .ExpectChildMeasure(new Size(100, 400), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("Forwarded from child")); } #endregion } } ================================================ FILE: Source/QuestPDF.UnitTests/DecorationTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class DecorationTests { private Decoration CreateDecoration(TestPlan testPlan) { return new Decoration { Before = testPlan.CreateChild("before"), Content = testPlan.CreateChild("content"), After = testPlan.CreateChild("after"), }; } #region Measure [Test] public void Measure_ReturnsWrap_WhenBeforeReturnsWrap() { TestPlan .For(CreateDecoration) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("before", new Size(400, 300), SpacePlan.Wrap("Mock")) .ExpectChildMeasure("after", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("content", new Size(400, 250), SpacePlan.FullRender(100, 100)) .CheckMeasureResult(SpacePlan.Wrap("Decoration slot (before or after) does not fit fully on the page.")); } [Test] public void Measure_ReturnsWrap_WhenContentReturnsWrap() { TestPlan .For(CreateDecoration) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("before", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("after", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("content", new Size(400, 200), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("The primary content does not fit on the page.")); } [Test] public void Measure_ReturnsWrap_WhenAfterReturnsWrap() { TestPlan .For(CreateDecoration) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("before", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("after", new Size(400, 300), SpacePlan.Wrap("Mock")) .ExpectChildMeasure("content", new Size(400, 250), SpacePlan.FullRender(100, 100)) .CheckMeasureResult(SpacePlan.Wrap("Decoration slot (before or after) does not fit fully on the page.")); } [Test] public void Measure_ReturnsWrap_WhenBeforeReturnsPartialRender() { TestPlan .For(CreateDecoration) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("before", new Size(400, 300), SpacePlan.PartialRender(100, 50)) .ExpectChildMeasure("after", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("content", new Size(400, 250), SpacePlan.FullRender(100, 100)) .CheckMeasureResult(SpacePlan.Wrap("Decoration slot (before or after) does not fit fully on the page.")); } [Test] public void Measure_ReturnsWrap_WhenAfterReturnsPartialRender() { TestPlan .For(CreateDecoration) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("before", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("after", new Size(400, 300), SpacePlan.PartialRender(100, 50)) .ExpectChildMeasure("content", new Size(400, 250), SpacePlan.FullRender(100, 100)) .CheckMeasureResult(SpacePlan.Wrap("Decoration slot (before or after) does not fit fully on the page.")); } [Test] public void Measure_ReturnsWrap_WhenContentReturnsPartialRender() { TestPlan .For(CreateDecoration) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("before", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("after", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("content", new Size(400, 200), SpacePlan.PartialRender(150, 100)) .CheckMeasureResult(SpacePlan.PartialRender(150, 200)); } [Test] public void Measure_ReturnsWrap_WhenContentReturnsFullRender() { TestPlan .For(CreateDecoration) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure("before", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("after", new Size(400, 300), SpacePlan.FullRender(100, 50)) .ExpectChildMeasure("content", new Size(400, 200), SpacePlan.FullRender(150, 100)) .CheckMeasureResult(SpacePlan.FullRender(150, 200)); } #endregion #region Draw [Test] public void Draw_Append() { TestPlan .For(CreateDecoration) .DrawElement(new Size(400, 300)) .ExpectChildMeasure("before", new Size(400, 300), SpacePlan.FullRender(200, 40)) .ExpectChildMeasure("after", new Size(400, 300), SpacePlan.FullRender(200, 60)) .ExpectChildMeasure("content", new Size(400, 200), SpacePlan.FullRender(300, 100)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw("before", new Size(300, 40)) .ExpectCanvasTranslate(0, 0) .ExpectCanvasTranslate(0, 40) .ExpectChildDraw("content", new Size(300, 100)) .ExpectCanvasTranslate(0, -40) .ExpectCanvasTranslate(0, 140) .ExpectChildDraw("after", new Size(300, 60)) .ExpectCanvasTranslate(0, -140) .CheckDrawResult(); } #endregion } } ================================================ FILE: Source/QuestPDF.UnitTests/DocumentCompressionTests.cs ================================================ using System; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using NUnit.Framework; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests; public class DocumentCompressionTests { [Test] public void Test() { var document = Document.Create(document => { document.Page(page => { page.Size(PageSizes.A4); page.Content() .Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); columns.ConstantColumn(100); }); foreach (var y in Enumerable.Range(1, 100)) { foreach (var x in Enumerable.Range(1, 4)) { table .Cell() .Padding(5) .Border(1) .Background(Placeholders.BackgroundColor()) .Padding(5) .Text($"f({y}, {x}) = '{Placeholders.Sentence()}'"); } table .Cell() .Padding(5) .AspectRatio(2f) .Image(Placeholders.Image); } }); }); }); // warmup cache document.GeneratePdf(); var withoutCompression = MeasureDocumentSizeAndGenerationTime(false); var withCompression = MeasureDocumentSizeAndGenerationTime(true); var sizeRatio = withoutCompression.documentSize / (float)withCompression.documentSize; Assert.That(sizeRatio, Is.GreaterThan(3)); var generationTimeRatio = withCompression.generationTime / (float)withoutCompression.generationTime; Assert.That(generationTimeRatio, Is.LessThan(2)); (int documentSize, float generationTime) MeasureDocumentSizeAndGenerationTime(bool compress) { var stopwatch = new Stopwatch(); stopwatch.Restart(); var documentSize = document .WithSettings(new DocumentSettings { CompressDocument = compress }) .GeneratePdf() .Length; stopwatch.Stop(); return (documentSize, stopwatch.ElapsedMilliseconds); } } } ================================================ FILE: Source/QuestPDF.UnitTests/DocumentOperationTests.cs ================================================ using System; using System.IO; using System.Linq; using System.Runtime.InteropServices; using NUnit.Framework; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests; /// /// This test suite focus on executing various QPDF operations. /// In most cases, it does not check the result but rather if any exception is thrown. /// public class DocumentOperationTests { [Test] public void TakePages() { GenerateSampleDocument("take-input.pdf", Colors.Red.Medium, 10); DocumentOperation .LoadFile("take-input.pdf") .TakePages("2-5") .Save("operation-take.pdf"); } [Test] public void MergeTest() { GenerateSampleDocument("merge-first.pdf", Colors.Red.Medium, 3); GenerateSampleDocument("merge-second.pdf", Colors.Green.Medium, 5); GenerateSampleDocument("merge-third.pdf", Colors.Blue.Medium, 7); DocumentOperation .LoadFile("merge-first.pdf") .MergeFile("merge-second.pdf") .MergeFile("merge-third.pdf") .Save("operation-merged.pdf"); } [Test] public void OverlayTest() { GenerateSampleDocument("overlay-main.pdf", Colors.Red.Medium, 10); GenerateSampleDocument("overlay-watermark.pdf", Colors.Green.Medium, 5); DocumentOperation .LoadFile("overlay-main.pdf") .OverlayFile(new DocumentOperation.LayerConfiguration { FilePath = "overlay-watermark.pdf" }) .Save("operation-overlay.pdf"); } [Test] public void UnderlayTest() { GenerateSampleDocument("underlay-main.pdf", Colors.Red.Medium, 10); GenerateSampleDocument("underlay-watermark.pdf", Colors.Green.Medium, 5); DocumentOperation .LoadFile("underlay-main.pdf") .UnderlayFile(new DocumentOperation.LayerConfiguration { FilePath = "underlay-watermark.pdf", }) .Save("operation-underlay.pdf"); } [Test] public void AttachmentTest() { GenerateSampleDocument("attachment-main.pdf", Colors.Red.Medium, 10); GenerateSampleDocument("attachment-file.pdf", Colors.Green.Medium, 5); DocumentOperation .LoadFile("attachment-main.pdf") .AddAttachment(new DocumentOperation.DocumentAttachment { FilePath = "attachment-file.pdf" }) .Save("operation-attachment.pdf"); } [Test] public void Encrypt40Test() { GenerateSampleDocument("encrypt40-input.pdf", Colors.Red.Medium, 10); DocumentOperation .LoadFile("encrypt40-input.pdf") .Encrypt(new DocumentOperation.Encryption40Bit() { UserPassword = "user_password", OwnerPassword = "owner_password" }) .Save("operation-encrypt40.pdf"); } [Test] public void Encrypt128Test() { GenerateSampleDocument("encrypt128-input.pdf", Colors.Red.Medium, 10); DocumentOperation .LoadFile("encrypt128-input.pdf") .Encrypt(new DocumentOperation.Encryption128Bit() { UserPassword = "user_password", OwnerPassword = "owner_password" }) .Save("operation-encrypt128.pdf"); } [Test] public void Encrypt256Test() { GenerateSampleDocument("encrypt256-input.pdf", Colors.Red.Medium, 10); DocumentOperation .LoadFile("encrypt256-input.pdf") .Encrypt(new DocumentOperation.Encryption256Bit() { UserPassword = "user_password", OwnerPassword = "owner_password" }) .Save("operation-encrypt256.pdf"); } [Test] public void LinearizeTest() { GenerateSampleDocument("linearize-input.pdf", Colors.Red.Medium, 10); DocumentOperation .LoadFile("linearize-input.pdf") .Linearize() .Save("operation-linearize.pdf"); } [Test] public void DecryptTest() { GenerateSampleDocument("decrypt-input-not-encrypted.pdf", Colors.Red.Medium, 10); DocumentOperation .LoadFile("decrypt-input-not-encrypted.pdf") .Encrypt(new DocumentOperation.Encryption256Bit() { UserPassword = "user_password", OwnerPassword = "owner_password" }) .Save("decrypt-input-encrypted.pdf"); DocumentOperation .LoadFile("decrypt-input-encrypted.pdf", "owner_password") .Decrypt() .Save("operation-decrypt.pdf"); } [Test] public void RemoveRestrictionsTest() { GenerateSampleDocument("remove-restrictions-input-not-encrypted.pdf", Colors.Red.Medium, 10); DocumentOperation .LoadFile("remove-restrictions-input-not-encrypted.pdf") .Encrypt(new DocumentOperation.Encryption256Bit() { UserPassword = string.Empty, OwnerPassword = "owner_password", AllowPrinting = false, AllowContentExtraction = false }) .Save("remove-restrictions-input-encrypted.pdf"); DocumentOperation .LoadFile("remove-restrictions-input-encrypted.pdf", "owner_password") .RemoveRestrictions() .Save("operation-remove-restrictions.pdf"); } [Test] public void LoadEncryptedWithIncorrectPasswordTest() { GenerateSampleDocument("load-encrypted-input-not-encrypted.pdf", Colors.Red.Medium, 10); DocumentOperation .LoadFile("load-encrypted-input-not-encrypted.pdf") .Encrypt(new DocumentOperation.Encryption256Bit() { UserPassword = "user_password", OwnerPassword = "owner_password" }) .Save("load-encrypted-input-encrypted.pdf"); Assert.Catch(() => { DocumentOperation .LoadFile("load-encrypted-input-encrypted.pdf", "wrong_password") .Save("operation-load-encrypted.pdf"); }); } [Test] public void ExtendMetadataTest() { GenerateSampleDocument("extend-metadata-input.pdf", Colors.Red.Medium, 10); // requires PDF/A-3b DocumentOperation .LoadFile("extend-metadata-input.pdf") .ExtendMetadata("") .Save("operation-extend-metadata.pdf"); } private void GenerateSampleDocument(string filePath, Color color, int length) { Document .Create(document => { document.Page(page => { page.Size(PageSizes.A4); page.PageColor(Colors.Transparent); page.Content().Column(column => { foreach (var i in Enumerable.Range(1, length)) { if (i != 1) column.Item().PageBreak(); var width = Random.Shared.Next(100, 200); var height = Random.Shared.Next(100, 200); var horizontalTranslation = Random.Shared.Next(0, (int)PageSizes.A4.Width - width); var verticalTranslation = Random.Shared.Next(0, (int)PageSizes.A4.Height - height); column.Item() .TranslateX(horizontalTranslation) .TranslateY(verticalTranslation) .Width(width) .Height(height) .Background(color.WithAlpha(64)) .AlignCenter() .AlignMiddle() .Text($"{filePath}\npage {i}") .FontColor(color) .Bold() .FontSize(16); } }); }); }) .WithSettings(new DocumentSettings { PdfA = true }) .GeneratePdf(filePath); } } ================================================ FILE: Source/QuestPDF.UnitTests/DynamicImageTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; using SkiaSharp; namespace QuestPDF.UnitTests { [TestFixture] public class DynamicImageTests { [Test] public void Measure_TakesAvailableSpaceRegardlessOfSize() { TestPlan .For(x => new DynamicImage { TargetDpi = DocumentSettings.DefaultRasterDpi, CompressionQuality = ImageCompressionQuality.High, Source = payload => GenerateImage(payload.ImageSize) }) .MeasureElement(new Size(300, 200)) .CheckMeasureResult(SpacePlan.FullRender(300, 200)); } [Test] public void Draw_HandlesNull() { TestPlan .For(x => new DynamicImage { TargetDpi = DocumentSettings.DefaultRasterDpi, CompressionQuality = ImageCompressionQuality.High, Source = size => null }) .DrawElement(new Size(300, 200)) .CheckDrawResult(); } [Test] public void Draw_PreservesSize() { TestPlan .For(x => new DynamicImage { TargetDpi = DocumentSettings.DefaultRasterDpi, CompressionQuality = ImageCompressionQuality.High, Source = payload => GenerateImage(payload.ImageSize) }) .DrawElement(new Size(300, 200)) .ExpectCanvasDrawImage(Position.Zero, new Size(300, 200)) .CheckDrawResult(); } [Test] public void Draw_PassesCorrectSizeToSource() { ImageSize passedSize = default; TestPlan .For(x => new DynamicImage { TargetDpi = DocumentSettings.DefaultRasterDpi * 3, CompressionQuality = ImageCompressionQuality.High, Source = payload => { passedSize = payload.ImageSize; return GenerateImage(payload.ImageSize); } }) .DrawElement(new Size(400, 300)) .ExpectCanvasDrawImage(Position.Zero, new Size(400, 300)) .CheckDrawResult(); Assert.That(passedSize.Width, Is.EqualTo(1200)); Assert.That(passedSize.Height, Is.EqualTo(900)); } byte[] GenerateImage(ImageSize size) { var image = GenerateImage(size.Width, size.Height); return image.Encode(SKEncodedImageFormat.Png, 100).ToArray(); } static SKImage GenerateImage(int width, int height) { var imageInfo = new SKImageInfo(width, height); using var surface = SKSurface.Create(imageInfo); return surface.Snapshot(); } } } ================================================ FILE: Source/QuestPDF.UnitTests/EnsureSpaceTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class EnsureSpaceTests { [Test] public void Measure_ReturnsPartialRenderWithZeroSize_WhenChildReturnsWrap() { TestPlan .For(x => new EnsureSpace { Child = x.CreateChild(), MinHeight = 200 }) .MeasureElement(new Size(400, 100)) .ExpectChildMeasure(new Size(400, 100), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.PartialRender(Size.Zero)); } [Test] public void Measure_ReturnsPartialRenderWithZeroSize_WhenChildReturnsPartialRender_AndNotEnoughSpace() { TestPlan .For(x => new EnsureSpace { Child = x.CreateChild(), MinHeight = 200 }) .MeasureElement(new Size(400, 100)) .ExpectChildMeasure(new Size(400, 100), SpacePlan.PartialRender(300, 50)) .CheckMeasureResult(SpacePlan.PartialRender(Size.Zero)); } [Test] public void Measure_ReturnsPartialRender_WhenChildReturnsPartialRender_AndEnoughSpace() { TestPlan .For(x => new EnsureSpace { Child = x.CreateChild(), MinHeight = 200 }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(new Size(400, 300), SpacePlan.PartialRender(300, 250)) .CheckMeasureResult(SpacePlan.PartialRender(300, 250)); } [Test] public void Measure_ReturnsFullRender_WhenChildReturnsFullRender_AndNotEnoughSpace() { TestPlan .For(x => new EnsureSpace { Child = x.CreateChild(), MinHeight = 200 }) .MeasureElement(new Size(400, 100)) .ExpectChildMeasure(new Size(400, 100), SpacePlan.FullRender(300, 50)) .CheckMeasureResult(SpacePlan.FullRender(300, 50)); } [Test] public void Measure_ReturnsFullRender_WhenChildReturnsFullRender_AndEnoughSpace() { TestPlan .For(x => new EnsureSpace { Child = x.CreateChild(), MinHeight = 200 }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(new Size(400, 300), SpacePlan.FullRender(300, 250)) .CheckMeasureResult(SpacePlan.FullRender(300, 250)); } } } ================================================ FILE: Source/QuestPDF.UnitTests/ExtendTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class ExtendTests { [Test] public void Measure_ReturnsWrap_WhenChildReturnsWrap() { TestPlan .For(x => new Extend { Child = x.CreateChild() }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("Forwarded from child")); } [Test] public void Measure_ReturnsPartialRender_WhenChildReturnsPartialRender() { TestPlan .For(x => new Extend { Child = x.CreateChild(), ExtendHorizontal = true, ExtendVertical = true }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.PartialRender(300, 100)) .CheckMeasureResult(SpacePlan.PartialRender(400, 200)); } [Test] public void Measure_ReturnsFullRender_WhenChildReturnsFullRender() { TestPlan .For(x => new Extend { Child = x.CreateChild(), ExtendHorizontal = true, ExtendVertical = true }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(300, 100)) .CheckMeasureResult(SpacePlan.FullRender(400, 200)); } [Test] public void Measure_ExtendHorizontal() { TestPlan .For(x => new Extend { Child = x.CreateChild(), ExtendHorizontal = true, ExtendVertical = false }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(100, 100)) .CheckMeasureResult(SpacePlan.FullRender(400, 100)); } [Test] public void Measure_ExtendVertical() { TestPlan .For(x => new Extend { Child = x.CreateChild(), ExtendHorizontal = false, ExtendVertical = true }) .MeasureElement(new Size(400, 200)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(100, 100)) .CheckMeasureResult(SpacePlan.FullRender(100, 200)); } [Test] public void Draw() => SimpleContainerTests.Draw(); } } ================================================ FILE: Source/QuestPDF.UnitTests/ExternalLinkTests.cs ================================================ using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class ExternalLinkTests { [Test] public void Measure() => SimpleContainerTests.Measure(); // TODO: consider tests for the Draw method } } ================================================ FILE: Source/QuestPDF.UnitTests/FontManagerTests.cs ================================================ using System; using System.IO; using NUnit.Framework; using QuestPDF.Drawing; namespace QuestPDF.UnitTests { public class FontManagerTests { [Test] public void LoadFontFromFile() { using var stream = File.OpenRead("Resources/FontContent.ttf"); FontManager.RegisterFont(stream); } [Test] public void LoadFontFromEmbeddedResource() { FontManager.RegisterFontFromEmbeddedResource("QuestPDF.UnitTests.Resources.FontEmbeddedResource.ttf"); } [Test] public void LoadFontFromEmbeddedResource_ShouldThrowException_WhenResourceIsNotAvailable() { Assert.Throws(() => { FontManager.RegisterFontFromEmbeddedResource("QuestPDF.UnitTests.WrongPath.ttf"); }); } } } ================================================ FILE: Source/QuestPDF.UnitTests/ImageGenerationTests.cs ================================================ using System; using System.IO; using System.Linq; using NUnit.Framework; using NUnit.Framework.Legacy; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using SkiaSharp; namespace QuestPDF.UnitTests { public class GenerateImageTests { [Test] [TestCaseSource(nameof(GeneratedImageResolutionCorrespondsToTargetDpi_TestCases))] public void GeneratedImageResolutionCorrespondsToTargetDpi(GeneratedImageResolutionCorrespondsToTargetDpi_TestCaseItem testCase) { // act var images = Document .Create(document => document.Page(page => { page.Size(testCase.PageSize); page.Content().Text("Test"); })) .GenerateImages(new ImageGenerationSettings { RasterDpi = testCase.TargetDpi }) .ToList(); // assert Assert.That(images, Has.Exactly(1).Items); var imageData = images.First(); Assert.That(imageData, Is.Not.Null); using var image = SKImage.FromEncodedData(imageData); Assert.That(image, Is.Not.Null); Assert.That(image.Width, Is.EqualTo(testCase.ExpectedImageSize.Width)); Assert.That(image.Height, Is.EqualTo(testCase.ExpectedImageSize.Height)); } public record GeneratedImageResolutionCorrespondsToTargetDpi_TestCaseItem(PageSize PageSize, int TargetDpi, ImageSize ExpectedImageSize); public static GeneratedImageResolutionCorrespondsToTargetDpi_TestCaseItem[] GeneratedImageResolutionCorrespondsToTargetDpi_TestCases = { new(new PageSize(150, 250), 72, new ImageSize(150, 250)), new(new PageSize(200, 300), 144, new ImageSize(400, 600)), new(new PageSize(250, 350), 360, new ImageSize(1250, 1750)), }; [Test] public void GeneratedImageSizeCorrespondsToImageQuality() { // arrange var document = Document.Create(document => document.Page(page => { page.Content().Image("Resources/photo.jpg"); })); // act var imageSizeWithLowQuality = CheckImageSize(ImageCompressionQuality.Low); var imageSizeWithMediumQuality = CheckImageSize(ImageCompressionQuality.Medium); var imageSizeWithHighQuality = CheckImageSize(ImageCompressionQuality.High); // assert Assert.That(imageSizeWithLowQuality, Is.LessThan(imageSizeWithMediumQuality)); Assert.That(imageSizeWithMediumQuality, Is.LessThan(imageSizeWithHighQuality)); int CheckImageSize(ImageCompressionQuality quality) { var images = document .GenerateImages(new ImageGenerationSettings() { ImageFormat = ImageFormat.Jpeg, ImageCompressionQuality = quality }) .ToList(); Assert.That(images, Has.Exactly(1).Items); var image = images.First(); Assert.That(image, Is.Not.Null); return image.Length; } } [TestCase(ImageFormat.Png)] [TestCase(ImageFormat.Jpeg)] [TestCase(ImageFormat.Webp)] public void ImageFormatIsRespected(ImageFormat imageFormat) { var images = Document .Create(document => { document.Page(page => { page.Content().Padding(25).AspectRatio(2).Background(Colors.Red.Medium); }); }) .GenerateImages(new ImageGenerationSettings() { ImageFormat = imageFormat }) .ToList(); Assert.That(images, Has.Exactly(1).Items); var imageData = images.First(); Assert.That(imageData, Is.Not.Null); using var imageStream = new MemoryStream(imageData); using var imageCodec = SKCodec.Create(imageStream); Assert.That(imageCodec, Is.Not.Null); Assert.That(imageCodec.EncodedFormat.ToString(), Is.EqualTo(imageFormat.ToString())); } } } ================================================ FILE: Source/QuestPDF.UnitTests/ImageTests.cs ================================================ using System; using System.IO; using System.Linq; using System.Net.Mime; using System.Threading; using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Drawing.Exceptions; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; using SkiaSharp; using ImageElement = QuestPDF.Elements.Image; using DocumentImage = QuestPDF.Infrastructure.Image; namespace QuestPDF.UnitTests { [TestFixture] public class ImageTests { [Test] public void Measure_TakesMinimalSpaceRegardlessOfSize() { TestPlan .For(x => new ImageElement { DocumentImage = GenerateDocumentImage(400, 300) }) .MeasureElement(new Size(300, 200)) .CheckMeasureResult(SpacePlan.FullRender(0, 0)); } [Test] public void Draw_TakesAvailableSpaceRegardlessOfSize() { TestPlan .For(x => new ImageElement { CompressionQuality = ImageCompressionQuality.High, TargetDpi = DocumentSettings.DefaultRasterDpi, DocumentImage = GenerateDocumentImage(400, 300) }) .DrawElement(new Size(300, 200)) .ExpectCanvasDrawImage(new Position(0, 0), new Size(300, 200)) .CheckDrawResult(); } [Test] public void Fluent_RecognizesImageProportions() { var image = GenerateDocumentImage(60, 20); TestPlan .For(x => { var container = new Container(); container.Image(image); return container; }) .MeasureElement(new Size(300, 200)) .CheckMeasureResult(SpacePlan.FullRender(300, 100)); } [Test] public void ImageObject_ThrowsEncodingException_WhenImageDataIsIncorrect() { Func action = () => Infrastructure.Image.FromBinaryData(new byte[] { 1, 2, 3 }); Assert.That(action, Throws.Exception.TypeOf().With.Message.EqualTo("Cannot decode the provided image.")); } [Test] public void ImageObject_ThrowsEncodingException_WhenStreamIsIncorrect() { Func action = () => { using var stream = new MemoryStream(new byte[] { 1, 2, 3 }); return Infrastructure.Image.FromStream(stream); }; Assert.That(action, Throws.Exception.TypeOf().With.Message.EqualTo("Cannot decode the provided image.")); } [Test] public void ImageObject_ThrowsFileNotFoundException_FileIsNotFound() { var action = () => Infrastructure.Image.FromFile("non-existing-file.jpg"); Assert.That(action, Throws.Exception.TypeOf().With.Message.EqualTo("Cannot load an image under the provided relative path, file not found: non-existing-file.jpg")); } [Test] public void UsingSharedImageShouldNotDrasticallyIncreaseDocumentSize() { var photo = File.ReadAllBytes("Resources/photo.jpg"); var documentWithSingleImageSize = GetDocumentSize(container => { container.Image(photo); }); var documentWithMultipleImagesSize = GetDocumentSize(container => { container.Column(column => { foreach (var i in Enumerable.Range(0, 10)) column.Item().Image(photo); }); }); using var sharedImage = DocumentImage.FromBinaryData(photo); var documentWithSingleImageUsedMultipleTimesSize = GetDocumentSize(container => { container.Column(column => { foreach (var i in Enumerable.Range(0, 10)) column.Item().Image(sharedImage); }); }); var documentWithMultipleImagesSizeRatio = (documentWithMultipleImagesSize / (float)documentWithSingleImageSize); Assert.That(documentWithMultipleImagesSizeRatio, Is.InRange(9.9f, 10)); var documentWithSingleImageUsedMultipleTimesSizeRatio = (documentWithSingleImageUsedMultipleTimesSize / (float)documentWithSingleImageSize); Assert.That(documentWithSingleImageUsedMultipleTimesSizeRatio, Is.InRange(1f, 1.05f)); } [Test] public void ImageCompressionHasImpactOnDocumentSize() { var photo = File.ReadAllBytes("Resources/photo.jpg"); var veryLowCompressionSize = GetDocumentSize(container => container.Image(photo).WithCompressionQuality(ImageCompressionQuality.VeryLow)); var bestCompressionSize = GetDocumentSize(container => container.Image(photo).WithCompressionQuality(ImageCompressionQuality.Best)); var compressionSizeRatio = (bestCompressionSize / (float)veryLowCompressionSize); Assert.That(compressionSizeRatio, Is.GreaterThan(10)); } [Test] public void TargetDpiHasImpactOnDocumentSize() { var photo = File.ReadAllBytes("Resources/photo.jpg"); var lowDpiSize = GetDocumentSize(container => container.Image(photo).WithRasterDpi(12)); var highDpiSize = GetDocumentSize(container => container.Image(photo).WithRasterDpi(144)); var dpiSizeRatio = (highDpiSize / (float)lowDpiSize); Assert.That(dpiSizeRatio, Is.GreaterThan(35)); } private static int GetDocumentSize(Action container) { return Document .Create(document => { document.Page(page => { page.Content().Element(container); }); }) .GeneratePdf() .Length; } static DocumentImage GenerateDocumentImage(int width, int height) { var image = Placeholders.Image(width, height); var result = DocumentImage.FromBinaryData(image); result.IsShared = false; return result; } } } ================================================ FILE: Source/QuestPDF.UnitTests/InternalLinkTests.cs ================================================ using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class InternalLinkTests { [Test] public void Measure() => SimpleContainerTests.Measure(); // TODO: consider tests for the Draw method } } ================================================ FILE: Source/QuestPDF.UnitTests/InternalLocationTests.cs ================================================ using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class InternalLocationTests { [Test] public void Measure() => SimpleContainerTests.Measure(); // TODO: consider tests for the Draw method } } ================================================ FILE: Source/QuestPDF.UnitTests/LayersTests.cs ================================================ using System.Collections.Generic; using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class LayersTests { private const string BackgroundLayer = "background"; private const string MainLayer = "main"; private const string ForegroundLayer = "foreground"; private static Layers GetLayers(TestPlan x) { return new Layers { Children = new List { new Layer { Child = x.CreateChild(BackgroundLayer) }, new Layer { Child = x.CreateChild(MainLayer), IsPrimary = true }, new Layer { Child = x.CreateChild(ForegroundLayer) } } }; } #region measure [Test] public void Measure_Wrap() { TestPlan .For(GetLayers) .MeasureElement(new Size(800, 600)) .ExpectChildMeasure(MainLayer, new Size(800, 600), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("The content of the primary layer does not fit (even partially) the available space.")); } [Test] public void Measure_PartialRender() { TestPlan .For(GetLayers) .MeasureElement(new Size(800, 600)) .ExpectChildMeasure(MainLayer, new Size(800, 600), SpacePlan.PartialRender(700, 500)) .CheckMeasureResult(SpacePlan.PartialRender(700, 500)); } [Test] public void Measure_FullRender() { TestPlan .For(GetLayers) .MeasureElement(new Size(800, 600)) .ExpectChildMeasure(MainLayer, new Size(800, 600), SpacePlan.FullRender(500, 400)) .CheckMeasureResult(SpacePlan.FullRender(500, 400)); } #endregion #region draw [Test] public void Draw_Simple() { TestPlan .For(GetLayers) .MeasureElement(new Size(800, 600)) .ExpectChildMeasure(BackgroundLayer, new Size(800, 600), SpacePlan.FullRender(100, 200)) .ExpectChildMeasure(MainLayer, new Size(800, 600), SpacePlan.PartialRender(200, 300)) .ExpectChildMeasure(ForegroundLayer, new Size(800, 600), SpacePlan.FullRender(300, 400)) .ExpectChildDraw(BackgroundLayer, new Size(800, 600)) .ExpectChildDraw(MainLayer, new Size(800, 600)) .ExpectChildDraw(ForegroundLayer, new Size(800, 600)) .CheckDrawResult(); } [Test] public void Draw_WhenSecondaryLayerReturnsWrap_SkipThatLayer_1() { TestPlan .For(GetLayers) .MeasureElement(new Size(800, 600)) .ExpectChildMeasure(BackgroundLayer, new Size(800, 600), SpacePlan.PartialRender(100, 200)) .ExpectChildMeasure(MainLayer, new Size(800, 600), SpacePlan.PartialRender(200, 300)) .ExpectChildMeasure(ForegroundLayer, new Size(800, 600), SpacePlan.Wrap("Mock")) .ExpectChildDraw(BackgroundLayer, new Size(800, 600)) .ExpectChildDraw(MainLayer, new Size(800, 600)) .CheckDrawResult(); } [Test] public void Draw_WhenSecondaryLayerReturnsWrap_SkipThatLayer_2() { TestPlan .For(GetLayers) .MeasureElement(new Size(800, 600)) .ExpectChildMeasure(BackgroundLayer, new Size(800, 600), SpacePlan.Wrap("Mock")) .ExpectChildMeasure(MainLayer, new Size(800, 600), SpacePlan.PartialRender(200, 300)) .ExpectChildMeasure(ForegroundLayer, new Size(800, 600), SpacePlan.PartialRender(300, 400)) .ExpectChildDraw(MainLayer, new Size(800, 600)) .ExpectChildDraw(ForegroundLayer, new Size(800, 600)) .CheckDrawResult(); } #endregion } } ================================================ FILE: Source/QuestPDF.UnitTests/LicenseSetup.cs ================================================ using NUnit.Framework; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests { [SetUpFixture] public class LicenseSetup { [OneTimeSetUp] public static void Setup() { QuestPDF.Settings.License = LicenseType.Community; } } } ================================================ FILE: Source/QuestPDF.UnitTests/LineTests.cs ================================================ using System; using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests; public class LineTests { #region Line Types [Test] public void VerticalLineTypeIsSetCorrectly() { var container = EmptyContainer.Create(); container.LineVertical(2); var line = container.Child as Line; Assert.That(line?.Type, Is.EqualTo(LineType.Vertical)); Assert.That(line?.Thickness, Is.EqualTo(2)); } [Test] public void HorizontalLineTypeIsSetCorrectly() { var container = EmptyContainer.Create(); container.LineHorizontal(3); var line = container.Child as Line; Assert.That(line?.Type, Is.EqualTo(LineType.Horizontal)); Assert.That(line?.Thickness, Is.EqualTo(3)); } #endregion #region Line Thickness [Test] public void VerticalLineThicknessSupportsUnitConversion() { var container = EmptyContainer.Create(); container.LineVertical(2, Unit.Inch); var line = container.Child as Line; Assert.That(line?.Thickness, Is.EqualTo(144)); } [TestCase(-5f)] [TestCase(-float.Epsilon)] public void LineThicknessCannotBeNegative(float thickness) { var exception = Assert.Throws(() => { EmptyContainer .Create() .LineVertical(thickness); }); Assert.That(exception.Message, Is.EqualTo("The Line thickness cannot be negative. (Parameter 'thickness')")); } [Test] public void LineThicknessCanBeEqualToZero() { // thickness 0 corresponds to a hairline var container = EmptyContainer.Create(); container.LineHorizontal(0); var line = container.Child as Line; Assert.That(line?.Thickness, Is.Zero); } [Test] public void HorizontalLineThicknessSupportsUnitConversion() { var container = EmptyContainer.Create(); container.LineHorizontal(3, Unit.Inch); var line = container.Child as Line; Assert.That(line?.Thickness, Is.EqualTo(216)); } #endregion [Test] public void LineColorIsSetCorrectly() { var container = EmptyContainer.Create(); container.LineHorizontal(1).LineColor(Colors.Red.Medium); var line = container.Child as Line; Assert.That(line?.Color, Is.EqualTo(Colors.Red.Medium)); } #region Line Dash Pattern [Test] public void LineDashPatternCannotBeNull() { var exception = Assert.Throws(() => { EmptyContainer .Create() .LineVertical(1) .LineDashPattern(null); }); Assert.That(exception.Message, Is.EqualTo("The dash pattern cannot be null. (Parameter 'dashPattern')")); } [Test] public void LineDashPatternCannotBeEmpty() { var exception = Assert.Throws(() => { EmptyContainer .Create() .LineVertical(1) .LineDashPattern([]); }); Assert.That(exception.Message, Is.EqualTo("The dash pattern cannot be empty. (Parameter 'dashPattern')")); } [Test] public void LineDashPatternMustHaveEvenNumberOfElements() { var exception = Assert.Throws(() => { EmptyContainer .Create() .LineVertical(1) .LineDashPattern([ 1, 2, 3 ]); }); Assert.That(exception.Message, Is.EqualTo("The dash pattern must contain an even number of elements. (Parameter 'dashPattern')")); } [Test] public void LineDashPatternIsSetCorrectly() { var container = EmptyContainer.Create(); container .LineVertical(1) .LineDashPattern([1, 2, 3, 4]); var line = container.Child as Line; Assert.That(line?.DashPattern, Is.EquivalentTo([ 1, 2, 3, 4 ])); } [Test] public void LineDashPatternSupportsUnitConversion() { var container = EmptyContainer.Create(); container .LineVertical(1) .LineDashPattern([1, 2, 3, 4], Unit.Inch); var line = container.Child as Line; Assert.That(line?.DashPattern, Is.EquivalentTo([ 72, 144, 216, 288 ])); } #endregion #region Gradient Colors [Test] public void LineGradientColorsCannotBeBull() { var exception = Assert.Throws(() => { EmptyContainer .Create() .LineVertical(1) .LineGradient(null); }); Assert.That(exception.Message, Is.EqualTo("The gradient colors cannot be null. (Parameter 'colors')")); } [Test] public void LineGradientColorsCannotBeEmpty() { var exception = Assert.Throws(() => { EmptyContainer .Create() .LineVertical(1) .LineGradient([]); }); Assert.That(exception.Message, Is.EqualTo("The gradient colors cannot be empty. (Parameter 'colors')")); } [Test] public void LineGradientColorsAreSetCorrectly() { var container = EmptyContainer.Create(); container .LineVertical(1) .LineGradient([Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium]); var line = container.Child as Line; Assert.That(line?.GradientColors, Is.EquivalentTo([ Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium ])); } #endregion #region Companion Hint [Test] public void VerticalLineCompanionHint() { var container = EmptyContainer.Create(); container.LineVertical(123.45f); var line = container.Child as Line; Assert.That(line?.GetCompanionHint(), Is.EqualTo("Vertical 123.5")); } [Test] public void HorizontalLineCompanionHint() { var container = EmptyContainer.Create(); container.LineHorizontal(234.56f); var line = container.Child as Line; Assert.That(line?.GetCompanionHint(), Is.EqualTo("Horizontal 234.6")); } #endregion [Test] [Repeat(10)] public void LineSupportsStatefulOperations() { var container = EmptyContainer.Create(); container.LineHorizontal(1); var line = container.Child as Line; Assert.That(line.GetState(), Is.False); line.SetState(true); Assert.That(line.GetState(), Is.True); line.ResetState(); Assert.That(line.GetState(), Is.False); } } ================================================ FILE: Source/QuestPDF.UnitTests/PaddingTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class PaddingTests { [TestCase(0, 0, 0, 0, "")] [TestCase(10, 0, 0, 0, "L=10")] [TestCase(0, 15, 0, 0, "T=15")] [TestCase(0, 0, 20, 0, "R=20")] [TestCase(0, 0, 0, 25, "B=25")] [TestCase(50, 0, 50, 0, "H=50")] [TestCase(0, 60, 0, 60, "V=60")] [TestCase(10, -20, 10, -30, "L=10 T=-20 R=10 B=-30")] [TestCase(-5, -10, 15, -10, "L=-5 T=-10 R=15 B=-10")] [TestCase(1.234f, -2.345f, 3.456f, -4.567f, "L=1.2 T=-2.3 R=3.5 B=-4.6")] [TestCase(5, 5, 5, 5, "A=5")] public void CompanionHint(float left, float top, float right, float bottom, string expected) { var container = EmptyContainer.Create(); container .PaddingLeft(left) .PaddingTop(top) .PaddingRight(right) .PaddingBottom(bottom); var translationElement = container.Child as Padding; var companionHint = translationElement?.GetCompanionHint(); Assert.That(companionHint, Is.EqualTo(expected)); } #region Cumulative Property [Test] public void PaddingLeftIsCumulative() { var container = EmptyContainer.Create(); container.PaddingLeft(-20).PaddingLeft(25).PaddingLeft(30); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Left, Is.EqualTo(35)); } [Test] public void PaddingTopIsCumulative() { var container = EmptyContainer.Create(); container.PaddingTop(20).PaddingTop(-25).PaddingTop(30); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Top, Is.EqualTo(25)); } [Test] public void PaddingRightIsCumulative() { var container = EmptyContainer.Create(); container.PaddingRight(20).PaddingRight(25).PaddingRight(-30); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Right, Is.EqualTo(15)); } [Test] public void PaddingBottomIsCumulative() { var container = EmptyContainer.Create(); container.PaddingBottom(-20).PaddingBottom(-25).PaddingBottom(30); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Bottom, Is.EqualTo(-15)); } #endregion #region Simple Asignment [Test] public void PaddingVerticalShorthandWorksCorrectly() { var container = EmptyContainer.Create(); container.PaddingVertical(123); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Left, Is.Zero); Assert.That(rowContainer?.Top, Is.EqualTo(123)); Assert.That(rowContainer?.Right, Is.Zero); Assert.That(rowContainer?.Bottom, Is.EqualTo(123)); } [Test] public void PaddingHorizontalShorthandWorksCorrectly() { var container = EmptyContainer.Create(); container.PaddingHorizontal(234); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Left, Is.EqualTo(234)); Assert.That(rowContainer?.Top, Is.Zero); Assert.That(rowContainer?.Right, Is.EqualTo(234)); Assert.That(rowContainer?.Bottom, Is.Zero); } [Test] public void PaddingAllShorthandWorksCorrectly() { var container = EmptyContainer.Create(); container.Padding(456); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Left, Is.EqualTo(456)); Assert.That(rowContainer?.Top, Is.EqualTo(456)); Assert.That(rowContainer?.Right, Is.EqualTo(456)); Assert.That(rowContainer?.Bottom, Is.EqualTo(456)); } #endregion #region Unit Conversion [Test] public void PaddingLeftAppliesUnitConversion() { var container = EmptyContainer.Create(); container.PaddingLeft(2, Unit.Inch); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Left, Is.EqualTo(144)); } [Test] public void PaddingTopAppliesUnitConversion() { var container = EmptyContainer.Create(); container.PaddingTop(3, Unit.Inch); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Top, Is.EqualTo(216)); } [Test] public void PaddingRightAppliesUnitConversion() { var container = EmptyContainer.Create(); container.PaddingRight(4, Unit.Inch); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Right, Is.EqualTo(288)); } [Test] public void PaddingBottomAppliesUnitConversion() { var container = EmptyContainer.Create(); container.PaddingBottom(5, Unit.Inch); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Bottom, Is.EqualTo(360)); } [Test] public void PaddingVerticalAppliesUnitConversion() { var container = EmptyContainer.Create(); container.PaddingVertical(6, Unit.Inch); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Top, Is.EqualTo(432)); Assert.That(rowContainer?.Bottom, Is.EqualTo(432)); } [Test] public void PaddingHorizontalAppliesUnitConversion() { var container = EmptyContainer.Create(); container.PaddingHorizontal(7, Unit.Inch); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Left, Is.EqualTo(504)); Assert.That(rowContainer?.Right, Is.EqualTo(504)); } [Test] public void PaddingAllAppliesUnitConversion() { var container = EmptyContainer.Create(); container.Padding(8, Unit.Inch); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Left, Is.EqualTo(576)); Assert.That(rowContainer?.Top, Is.EqualTo(576)); Assert.That(rowContainer?.Right, Is.EqualTo(576)); Assert.That(rowContainer?.Bottom, Is.EqualTo(576)); } #endregion [Test] public void PaddingAppliesCorrectValues() { var container = EmptyContainer.Create(); container .PaddingLeft(20) .PaddingTop(25) .PaddingRight(30) .PaddingBottom(35) .PaddingVertical(-5) .PaddingHorizontal(-15) .Padding(10); var rowContainer = container.Child as Padding; Assert.That(rowContainer?.Left, Is.EqualTo(15)); Assert.That(rowContainer?.Top, Is.EqualTo(30)); Assert.That(rowContainer?.Right, Is.EqualTo(25)); Assert.That(rowContainer?.Bottom, Is.EqualTo(40)); } } } ================================================ FILE: Source/QuestPDF.UnitTests/PageBreakTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class PageBreakTests { [Test] public void Measure() { TestPlan .For(x => new PageBreak()) .MeasureElement(new Size(400, 300)) .CheckMeasureResult(SpacePlan.PartialRender(Size.Zero)) .DrawElement(new Size(400, 300)) .CheckDrawResult() .MeasureElement(new Size(500, 400)) .CheckMeasureResult(SpacePlan.Empty()); } } } ================================================ FILE: Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj ================================================ net10.0 en false true all runtime; build; native; contentfiles; analyzers; buildtransitive PreserveNewest PreserveNewest PreserveNewest ================================================ FILE: Source/QuestPDF.UnitTests/RotateTests.cs ================================================ using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests; public class RotateTests { [Test] public void RotateIsCumulative() { var container = EmptyContainer.Create(); container .Rotate(45) .Rotate(-15) .Rotate(20); var rotation = container.Child as Rotate; Assert.That(rotation?.Angle, Is.EqualTo(50)); } [TestCase(0, ExpectedResult = "No rotation")] [TestCase(45, ExpectedResult = "45 deg clockwise")] [TestCase(-75, ExpectedResult = "75 deg counter-clockwise")] [TestCase(12.345f, ExpectedResult = "12.3 deg clockwise")] public string RotateCompanionHint(float angle) { var container = EmptyContainer.Create(); container.Rotate(angle); var rotation = container.Child as Rotate; return rotation?.GetCompanionHint(); } } ================================================ FILE: Source/QuestPDF.UnitTests/RowTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests; [TestFixture] public class RowTests { #region Spacing [TestCase(float.MinValue)] [TestCase(-5)] [TestCase(-float.Epsilon)] public void NegativeSpacingThrowsException(float spacingValue) { var exception = Assert.Throws(() => { EmptyContainer .Create() .Row(row => { row.Spacing(spacingValue); }); }); Assert.That(exception.Message, Is.EqualTo("The row spacing cannot be negative. (Parameter 'spacing')")); } [TestCase(0)] [TestCase(float.Epsilon)] [TestCase(10)] public void ValidSpacingIsCorrectlyApplied(float spacingValue) { var container = EmptyContainer.Create(); container.Row(row => { row.Spacing(spacingValue); }); var rowContainer = container.Child as Row; Assert.That(rowContainer?.Spacing, Is.EqualTo(spacingValue)); } [Test] public void SpacingSupportsUnitConversion() { var container = EmptyContainer.Create(); container.Row(row => { row.Spacing(5, Unit.Inch); }); var rowContainer = container.Child as Row; Assert.That(rowContainer?.Spacing, Is.EqualTo(360)); } #endregion #region Relative Item [TestCase(-10)] [TestCase(-float.Epsilon)] [TestCase(0)] public void RelativeItemCannotHaveSizeSmallerOrEqualToZero(float size) { var exception = Assert.Throws(() => { EmptyContainer .Create() .Row(row => { row.RelativeItem(size); }); }); Assert.That(exception?.Message, Is.EqualTo("The relative item size must be greater than zero. (Parameter 'size')")); } [TestCase(float.Epsilon)] [TestCase(1)] [TestCase(5)] public void RelativeItemMustHaveSizeLargerThanZero(float size) { var container = EmptyContainer.Create(); container.Row(row => { row.RelativeItem(size); }); var rowContainer = container.Child as Row; Assert.That(rowContainer?.Items.Count, Is.EqualTo(1)); var firstItem = rowContainer?.Items.Single(); Assert.That(firstItem.Type, Is.EqualTo(RowItemType.Relative)); Assert.That(firstItem.Size, Is.EqualTo(size)); } #endregion #region Constant Item [TestCase(-10)] [TestCase(-float.Epsilon)] public void ConstantItemCannotHaveSizeSmallerThanZero(float size) { var exception = Assert.Throws(() => { EmptyContainer .Create() .Row(row => { row.ConstantItem(size); }); }); Assert.That(exception?.Message, Is.EqualTo("The constant item size cannot be negative. (Parameter 'size')")); } [TestCase(0)] [TestCase(100)] public void ConstantItemMustHaveSizeLargerOrEqualToZero(float size) { var container = EmptyContainer.Create(); container.Row(row => { row.ConstantItem(size); }); var rowContainer = container.Child as Row; Assert.That(rowContainer?.Items.Count, Is.EqualTo(1)); var firstItem = rowContainer?.Items.Single(); Assert.That(firstItem.Type, Is.EqualTo(RowItemType.Constant)); Assert.That(firstItem.Size, Is.EqualTo(size)); } [Test] public void ConstantItemSupportsUnitConversion() { var container = EmptyContainer.Create(); container.Row(row => { row.ConstantItem(2, Unit.Inch); }); var rowContainer = container.Child as Row; Assert.That(rowContainer?.Items.Count, Is.EqualTo(1)); var firstItem = rowContainer?.Items.Single(); Assert.That(firstItem.Type, Is.EqualTo(RowItemType.Constant)); Assert.That(firstItem.Size, Is.EqualTo(144)); } #endregion [Test] public void CompanionHints() { var container = EmptyContainer.Create(); container.Row(row => { row.RelativeItem(3); row.ConstantItem(2, Unit.Inch); row.AutoItem(); }); var rowContainer = container.Child as Row; Assert.That(rowContainer?.Items.Count, Is.EqualTo(3)); var items = rowContainer?.Items; Assert.That(items[0].GetCompanionHint(), Is.EqualTo("Relative 3")); Assert.That(items[1].GetCompanionHint(), Is.EqualTo("Constant 144")); Assert.That(items[2].GetCompanionHint(), Is.EqualTo("Auto")); } [Test] [Repeat(10)] public void RowSupportsStatefulOperations() { var container = EmptyContainer.Create(); container.Row(row => { foreach (var i in Enumerable.Range(0, 10)) row.RelativeItem(); }); var rowContainer = container.Child as Row; Assert.That(rowContainer?.Items.Count, Is.EqualTo(10)); rowContainer.ResetState(); Assert.That(rowContainer.GetState(), Is.EquivalentTo(new bool[10])); var newState = Enumerable .Range(0, 10) .Select(x => Random.Shared.Next() % 2 == 0) .ToArray(); rowContainer.SetState(newState); Assert.That(rowContainer.GetState(), Is.EquivalentTo(newState)); rowContainer.ResetState(); Assert.That(rowContainer.GetState(), Is.EquivalentTo(new bool[10])); } } ================================================ FILE: Source/QuestPDF.UnitTests/ScaleTests.cs ================================================ using System; using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class ScaleTests { [TestCase(2, 2, ExpectedResult = "A=2")] [TestCase(-3, -3, ExpectedResult = "A=-3")] [TestCase(4, 1, ExpectedResult = "H=4")] [TestCase(1, -2, ExpectedResult = "V=-2")] [TestCase(4, 5, ExpectedResult = "H=4 V=5")] [TestCase(1.2345f, -2.3456f, ExpectedResult = "H=1.2 V=-2.3")] public string CompanionHint(float horizontal, float vertical) { var container = EmptyContainer.Create(); container.ScaleHorizontal(horizontal).ScaleVertical(vertical); var translationElement = container.Child as Scale; return translationElement?.GetCompanionHint(); } #region Cumulative Property [Test] public void HorizontalScaleIsCumulative() { var container = EmptyContainer.Create(); container.ScaleHorizontal(3).ScaleHorizontal(0.5f).ScaleHorizontal(-4); var rowContainer = container.Child as Scale; Assert.That(rowContainer?.ScaleX, Is.EqualTo(-6)); Assert.That(rowContainer?.ScaleY, Is.EqualTo(1)); } [Test] public void VerticalScaleIsCumulative() { var container = EmptyContainer.Create(); container.ScaleVertical(2).ScaleVertical(-0.25f).ScaleVertical(-3f); var rowContainer = container.Child as Scale; Assert.That(rowContainer?.ScaleX, Is.EqualTo(1)); Assert.That(rowContainer?.ScaleY, Is.EqualTo(1.5f)); } [Test] public void ScaleIsCumulative() { var container = EmptyContainer.Create(); container.ScaleHorizontal(-5f).ScaleVertical(3).Scale(-0.25f); var rowContainer = container.Child as Scale; Assert.That(rowContainer?.ScaleX, Is.EqualTo(1.25f)); Assert.That(rowContainer?.ScaleY, Is.EqualTo(-0.75f)); } #endregion #region Flip [Test] public void FlipHorizontalAppliesCorrectScale() { var container = EmptyContainer.Create(); container.FlipHorizontal(); var rowContainer = container.Child as Scale; Assert.That(rowContainer?.ScaleX, Is.EqualTo(-1)); Assert.That(rowContainer?.ScaleY, Is.EqualTo(1)); } [Test] public void FlipVerticalAppliesCorrectScale() { var container = EmptyContainer.Create(); container.FlipVertical(); var rowContainer = container.Child as Scale; Assert.That(rowContainer?.ScaleX, Is.EqualTo(1)); Assert.That(rowContainer?.ScaleY, Is.EqualTo(-1)); } [Test] public void FlipOverAppliesCorrectScale() { var container = EmptyContainer.Create(); container.FlipOver(); var rowContainer = container.Child as Scale; Assert.That(rowContainer?.ScaleX, Is.EqualTo(-1)); Assert.That(rowContainer?.ScaleY, Is.EqualTo(-1)); } #endregion #region Zero Scale Validation [Test] public void ScaleCannotBeZero() { var exception = Assert.Throws(() => { EmptyContainer.Create().Scale(0); }); Assert.That(exception.Message, Is.EqualTo("Vertical scale factor cannot be zero. (Parameter 'factor')")); } [Test] public void VerticalScaleCannotBeZero() { var exception = Assert.Throws(() => { EmptyContainer.Create().ScaleVertical(0); }); Assert.That(exception.Message, Is.EqualTo("Vertical scale factor cannot be zero. (Parameter 'factor')")); } [Test] public void HorizontalScaleCannotBeZero() { var exception = Assert.Throws(() => { EmptyContainer.Create().ScaleHorizontal(0); }); Assert.That(exception.Message, Is.EqualTo("Vertical scale factor cannot be zero. (Parameter 'factor')")); } #endregion } } ================================================ FILE: Source/QuestPDF.UnitTests/ShowEntireTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class ShowEntireTests { [Test] public void Measure_ReturnsWrap_WhenElementReturnsWrap() { TestPlan .For(x => new ShowEntire { Child = x.CreateChild() }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(new Size(400, 300), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("Child element does not fit (even partially) on the page.")); } [Test] public void Measure_ReturnsWrap_WhenElementReturnsPartialRender() { TestPlan .For(x => new ShowEntire { Child = x.CreateChild() }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(new Size(400, 300), SpacePlan.PartialRender(300, 200)) .CheckMeasureResult(SpacePlan.Wrap("Child element fits only partially on the page.")); } [Test] public void Measure_ReturnsFullRender_WhenElementReturnsFullRender() { TestPlan .For(x => new ShowEntire { Child = x.CreateChild() }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(new Size(400, 300), SpacePlan.FullRender(300, 200)) .CheckMeasureResult(SpacePlan.FullRender(300, 200)); } [Test] public void Draw() => SimpleContainerTests.Draw(); } } ================================================ FILE: Source/QuestPDF.UnitTests/ShowOnceTest.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class ShowOnceTest { [Test] public void Draw() { TestPlan .For(x => new ShowOnce() { Child = x.CreateChild() }) // Measure the element and return result .MeasureElement(new Size(300, 200)) .ExpectChildMeasure("child", new Size(300, 200), SpacePlan.PartialRender(new Size(200, 200))) .CheckMeasureResult(SpacePlan.PartialRender(new Size(200, 200))) // Draw element partially .DrawElement(new Size(200, 200)) .ExpectChildMeasure(new Size(200, 200), SpacePlan.PartialRender(new Size(200, 200))) .ExpectChildDraw(new Size(200, 200)) .CheckDrawResult() // Element was not fully drawn // It should be measured again for rendering on next page .MeasureElement(new Size(800, 200)) .ExpectChildMeasure(new Size(800, 200), SpacePlan.FullRender(new Size(400, 200))) .CheckMeasureResult(SpacePlan.FullRender(new Size(400, 200))) // Draw element on next page // Element was fully drawn at this point .DrawElement(new Size(400, 200)) .ExpectChildMeasure(new Size(400, 200), SpacePlan.FullRender(new Size(400, 200))) .ExpectChildDraw(new Size(400, 200)) .CheckDrawResult() // In the next attempt of measuring element, it should behave like empty parent. .MeasureElement(new Size(600, 200)) .CheckMeasureResult(SpacePlan.Empty()) // In the next attempt of measuring element, it should not draw its child .DrawElement(new Size(600, 200)) .CheckDrawResult(); } } } ================================================ FILE: Source/QuestPDF.UnitTests/SimpleRotateTests.cs ================================================ using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests { [TestFixture] public class SimpleRotateTests { #region Cumulative rotation [Test] public void RotateRightIsCumulative() { var container = EmptyContainer.Create(); container .RotateRight() .RotateRight() .RotateRight() .RotateRight() .RotateRight(); var rotation = container.Child as SimpleRotate; Assert.That(rotation?.TurnCount, Is.EqualTo(5)); } [Test] public void RotateLeftIsCumulative() { var container = EmptyContainer.Create(); container .RotateLeft() .RotateLeft() .RotateLeft() .RotateLeft() .RotateLeft() .RotateLeft(); var rotation = container.Child as SimpleRotate; Assert.That(rotation?.TurnCount, Is.EqualTo(-6)); } [Test] public void RotateRightAndLeftCanBeCombined() { var container = EmptyContainer.Create(); container .RotateRight() .RotateRight() .RotateRight() .RotateRight() .RotateLeft() .RotateLeft() .RotateLeft(); var rotation = container.Child as SimpleRotate; Assert.That(rotation?.TurnCount, Is.EqualTo(1)); } #endregion #region Companion Hint [Test] public void NoRotationCompanionHint() { var container = EmptyContainer.Create(); container.RotateRight().RotateLeft(); var rotation = container.Child as SimpleRotate; Assert.That(rotation?.GetCompanionHint(), Is.EqualTo("No rotation")); } [Test] public void RotateRightCompanionHint() { var container = EmptyContainer.Create(); container.RotateRight(); var rotation = container.Child as SimpleRotate; Assert.That(rotation?.GetCompanionHint(), Is.EqualTo("90 deg clockwise")); } [Test] public void DoubleRotateLeftCompanionHint() { var container = EmptyContainer.Create(); container.RotateLeft().RotateLeft(); var rotation = container.Child as SimpleRotate; Assert.That(rotation?.GetCompanionHint(), Is.EqualTo("180 deg counter-clockwise")); } #endregion } } ================================================ FILE: Source/QuestPDF.UnitTests/StyledBoxTests.cs ================================================ using System; using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests; public class StyledBoxTests { [Test] public void BorderShorthandSetsCorrectValues() { var container = EmptyContainer.Create(); container.Border(1.23f, Colors.Amber.Darken2); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.EqualTo(1.23f)); Assert.That(styledBox?.BorderRight, Is.EqualTo(1.23f)); Assert.That(styledBox?.BorderTop, Is.EqualTo(1.23f)); Assert.That(styledBox?.BorderBottom, Is.EqualTo(1.23f)); Assert.That(styledBox?.BorderColor, Is.EqualTo(Colors.Amber.Darken2)); Assert.That(styledBox?.BackgroundColor, Is.EqualTo(Colors.Transparent)); Assert.That(styledBox?.BorderRadiusTopLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusTopRight, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomRight, Is.Zero); } #region Background [Test] public void BackgroundColorSetsCorrectValue() { var container = EmptyContainer.Create(); container.Background(Colors.Green.Medium); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BackgroundColor, Is.EqualTo(Colors.Green.Medium)); } [Test] public void BackgroundLinearGradientCannotBeEmpty() { var exception = Assert.Throws(() => { EmptyContainer.Create().BackgroundLinearGradient(123f, []); }); Assert.That(exception.Message, Does.Contain("The background linear-gradient colors cannot be empty. (Parameter 'colors')")); } [Test] public void BackgroundLinearGradientSetsCorrectValue() { var container = EmptyContainer.Create(); container.BackgroundLinearGradient(30f, [ Colors.Red.Lighten3, Colors.Orange.Lighten3, Colors.Yellow.Lighten3 ]); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BackgroundGradientAngle, Is.EqualTo(30)); Assert.That(styledBox?.BackgroundGradientColors, Is.EqualTo([ Colors.Red.Lighten3, Colors.Orange.Lighten3, Colors.Yellow.Lighten3 ])); } #endregion #region Thickness [Test] public void BorderAllSetsCorrectValue() { var container = EmptyContainer.Create(); container.Border(1.23f); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.EqualTo(1.23f)); Assert.That(styledBox?.BorderRight, Is.EqualTo(1.23f)); Assert.That(styledBox?.BorderTop, Is.EqualTo(1.23f)); Assert.That(styledBox?.BorderBottom, Is.EqualTo(1.23f)); } [Test] public void BorderAllSupportsUnitConversion() { var container = EmptyContainer.Create(); container.Border(0.25f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.EqualTo(18)); Assert.That(styledBox?.BorderRight, Is.EqualTo(18)); Assert.That(styledBox?.BorderTop, Is.EqualTo(18)); Assert.That(styledBox?.BorderBottom, Is.EqualTo(18)); } [Test] public void BorderVerticalSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderVertical(20); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.EqualTo(20)); Assert.That(styledBox?.BorderRight, Is.EqualTo(20)); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.Zero); } [Test] public void BorderVerticalSupportsUnitConversion() { var container = EmptyContainer.Create(); container.BorderVertical(0.5f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.EqualTo(36)); Assert.That(styledBox?.BorderRight, Is.EqualTo(36)); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.Zero); } [Test] public void BorderHorizontalSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderHorizontal(25); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.EqualTo(25)); Assert.That(styledBox?.BorderBottom, Is.EqualTo(25)); } [Test] public void BorderHorizontalSupportsUnitConversion() { var container = EmptyContainer.Create(); container.BorderHorizontal(0.75f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.EqualTo(54)); Assert.That(styledBox?.BorderBottom, Is.EqualTo(54)); } [Test] public void BorderLeftSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderLeft(5); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.EqualTo(5)); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.Zero); } [Test] public void BorderLeftSupportsUnitConversion() { var container = EmptyContainer.Create(); container.BorderLeft(0.25f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.EqualTo(18)); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.Zero); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void BorderLeftCannotBeNegative(float border) { var exception = Assert.Throws(() => { EmptyContainer.Create().BorderLeft(border); }); Assert.That(exception.Message, Does.Contain("The left border cannot be negative.")); } [Test] public void BorderRightSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderRight(10); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.EqualTo(10)); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.Zero); } [Test] public void BorderRightSupportsUnitConversion() { var container = EmptyContainer.Create(); container.BorderRight(0.5f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.EqualTo(36)); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.Zero); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void BorderRightCannotBeNegative(float border) { var exception = Assert.Throws(() => { EmptyContainer.Create().BorderRight(border); }); Assert.That(exception.Message, Does.Contain("The right border cannot be negative.")); } [Test] public void BorderTopSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderTop(15); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.EqualTo(15)); Assert.That(styledBox?.BorderBottom, Is.Zero); } [Test] public void BorderTopSupportsUnitConversion() { var container = EmptyContainer.Create(); container.BorderTop(0.75f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.EqualTo(54f)); Assert.That(styledBox?.BorderBottom, Is.Zero); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void BorderTopCannotBeNegative(float border) { var exception = Assert.Throws(() => { EmptyContainer.Create().BorderTop(border); }); Assert.That(exception.Message, Does.Contain("The top border cannot be negative.")); } [Test] public void BorderBottomSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderBottom(20); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.EqualTo(20)); } [Test] public void BorderBottomSupportsUnitConversion() { var container = EmptyContainer.Create(); container.BorderBottom(1f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.EqualTo(72f)); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void BorderBottomCannotBeNegative(float border) { var exception = Assert.Throws(() => { EmptyContainer.Create().BorderBottom(border); }); Assert.That(exception.Message, Does.Contain("The bottom border cannot be negative.")); } [Test] public void ZeroBorderIsSupported() { var container = EmptyContainer.Create(); container .BorderLeft(0) .BorderRight(0) .BorderTop(0) .BorderBottom(0); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.Zero); Assert.That(styledBox?.BorderRight, Is.Zero); Assert.That(styledBox?.BorderTop, Is.Zero); Assert.That(styledBox?.BorderBottom, Is.Zero); } [Test] public void BorderValuesAreOverridingEachOther() { var container = EmptyContainer.Create(); container .Border(5) .BorderVertical(10) .BorderLeft(15) .BorderBottom(20); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderLeft, Is.EqualTo(15)); Assert.That(styledBox?.BorderRight, Is.EqualTo(10)); Assert.That(styledBox?.BorderTop, Is.EqualTo(5)); Assert.That(styledBox?.BorderBottom, Is.EqualTo(20)); } [Test] public void BorderSetsColorToBlack() { var container = EmptyContainer.Create(); container.Border(5); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderColor, Is.EqualTo(Colors.Black)); } #endregion #region Corner Radius [Test] public void CornerRadiusAllSetsCorrectValue() { var container = EmptyContainer.Create(); container.CornerRadius(8); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.EqualTo(8)); Assert.That(styledBox?.BorderRadiusTopRight, Is.EqualTo(8)); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.EqualTo(8)); Assert.That(styledBox?.BorderRadiusBottomRight, Is.EqualTo(8)); } [Test] public void CornerRadiusAllSupportsUnitConversion() { var container = EmptyContainer.Create(); container.CornerRadius(1.5f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.EqualTo(108)); Assert.That(styledBox?.BorderRadiusTopRight, Is.EqualTo(108)); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.EqualTo(108)); Assert.That(styledBox?.BorderRadiusBottomRight, Is.EqualTo(108)); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void CornerRadiusAllCannotBeNegative(float cornerRadius) { var exception = Assert.Throws(() => { EmptyContainer.Create().CornerRadius(cornerRadius); }); Assert.That(exception.Message, Does.Contain("The top-left corner radius cannot be negative.")); } [Test] public void CornerRadiusTopLeftSetsCorrectValue() { var container = EmptyContainer.Create(); container.CornerRadiusTopLeft(8); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.EqualTo(8)); Assert.That(styledBox?.BorderRadiusTopRight, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomRight, Is.Zero); } [Test] public void CornerRadiusTopLeftSupportsUnitConversion() { var container = EmptyContainer.Create(); container.CornerRadiusTopLeft(0.25f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.EqualTo(18)); Assert.That(styledBox?.BorderRadiusTopRight, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomRight, Is.Zero); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void CornerRadiusTopLeftCannotBeNegative(float cornerRadius) { var exception = Assert.Throws(() => { EmptyContainer.Create().CornerRadiusTopLeft(cornerRadius); }); Assert.That(exception.Message, Does.Contain("The top-left corner radius cannot be negative.")); } [Test] public void CornerRadiusTopRightSetsCorrectValue() { var container = EmptyContainer.Create(); container.CornerRadiusTopRight(10); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusTopRight, Is.EqualTo(10)); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomRight, Is.Zero); } [Test] public void CornerRadiusTopRightSupportsUnitConversion() { var container = EmptyContainer.Create(); container.CornerRadiusTopRight(0.5f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusTopRight, Is.EqualTo(36)); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomRight, Is.Zero); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void CornerRadiusTopRightCannotBeNegative(float cornerRadius) { var exception = Assert.Throws(() => { EmptyContainer.Create().CornerRadiusTopRight(cornerRadius); }); Assert.That(exception.Message, Does.Contain("The top-right corner radius cannot be negative.")); } [Test] public void CornerRadiusBottomLeftSetsCorrectValue() { var container = EmptyContainer.Create(); container.CornerRadiusBottomLeft(12); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusTopRight, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.EqualTo(12)); Assert.That(styledBox?.BorderRadiusBottomRight, Is.Zero); } [Test] public void CornerRadiusBottomLeftSupportsUnitConversion() { var container = EmptyContainer.Create(); container.CornerRadiusBottomLeft(0.75f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusTopRight, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.EqualTo(54)); Assert.That(styledBox?.BorderRadiusBottomRight, Is.Zero); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void CornerRadiusBottomLeftCannotBeNegative(float cornerRadius) { var exception = Assert.Throws(() => { EmptyContainer.Create().CornerRadiusBottomLeft(cornerRadius); }); Assert.That(exception.Message, Does.Contain("The bottom-left corner radius cannot be negative.")); } [Test] public void CornerRadiusBottomRightSetsCorrectValue() { var container = EmptyContainer.Create(); container.CornerRadiusBottomRight(14); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusTopRight, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomRight, Is.EqualTo(14)); } [Test] public void CornerRadiusBottomRightSupportsUnitConversion() { var container = EmptyContainer.Create(); container.CornerRadiusBottomRight(1f, Unit.Inch); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusTopRight, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.Zero); Assert.That(styledBox?.BorderRadiusBottomRight, Is.EqualTo(72)); } [TestCase(-5)] [TestCase(-Size.Epsilon)] public void CornerRadiusBottomRightCannotBeNegative(float cornerRadius) { var exception = Assert.Throws(() => { EmptyContainer.Create().CornerRadiusBottomRight(cornerRadius); }); Assert.That(exception.Message, Does.Contain("The bottom-right corner radius cannot be negative.")); } [Test] public void CornerRadiusValuesAreOverridingEachOther() { var container = EmptyContainer.Create(); container .CornerRadius(4) .CornerRadiusTopLeft(6) .CornerRadiusTopRight(8) .CornerRadiusBottomLeft(10) .CornerRadiusBottomRight(12) .CornerRadiusBottomRight(14); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderRadiusTopLeft, Is.EqualTo(6)); Assert.That(styledBox?.BorderRadiusTopRight, Is.EqualTo(8)); Assert.That(styledBox?.BorderRadiusBottomLeft, Is.EqualTo(10)); Assert.That(styledBox?.BorderRadiusBottomRight, Is.EqualTo(14)); } #endregion #region Border Style [Test] public void BorderColorSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderColor(Colors.Red.Darken3); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderColor, Is.EqualTo(Colors.Red.Darken3)); } [Test] public void BorderLinearGradientCannotBeEmpty() { var exception = Assert.Throws(() => { EmptyContainer.Create().BorderLinearGradient(234f, []); }); Assert.That(exception.Message, Does.Contain("The border linear-gradient colors cannot be empty. (Parameter 'colors')")); } [Test] public void BorderLinearGradientSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderLinearGradient(123f, [ Colors.Red.Darken3, Colors.Orange.Darken3, Colors.Yellow.Darken3 ]); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderGradientAngle, Is.EqualTo(123)); Assert.That(styledBox?.BorderGradientColors, Is.EqualTo([ Colors.Red.Darken3, Colors.Orange.Darken3, Colors.Yellow.Darken3 ])); } #endregion #region Border Alignment [Test] public void BorderAlignmentIsMiddleWhenNoRoundedCorners() { var container = EmptyContainer.Create(); container.Border(5); var styledBox = container.Child as StyledBox; styledBox.AdjustBorderAlignment(); Assert.That(styledBox?.BorderAlignment, Is.EqualTo(0.5f)); } [Test] public void BorderAlignmentIsInsideWhenHasRoundedCorners() { var container = EmptyContainer.Create(); container.Border(5).CornerRadius(10); var styledBox = container.Child as StyledBox; styledBox.AdjustBorderAlignment(); Assert.That(styledBox?.BorderAlignment, Is.EqualTo(0f)); } [Test] public void BorderAlignmentInsideSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderAlignmentInside(); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderAlignment, Is.Zero); } [Test] public void BorderAlignmentMiddleSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderAlignmentMiddle(); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderAlignment, Is.EqualTo(0.5f)); } [Test] public void BorderAlignmentOutsideSetsCorrectValue() { var container = EmptyContainer.Create(); container.BorderAlignmentOutside(); var styledBox = container.Child as StyledBox; Assert.That(styledBox?.BorderAlignment, Is.EqualTo(1f)); } #endregion #region Shadow [Test] public void ShadowStyleSetsCorrectValue() { var container = EmptyContainer.Create(); container.Shadow(new BoxShadowStyle { Color = Colors.Black.WithAlpha(0.5f), OffsetX = 5, OffsetY = 10, Blur = 15, Spread = 20 }); var styledBox = container.Child as StyledBox; var shadow = styledBox?.Shadow; Assert.That(shadow.Color, Is.EqualTo(new Color(0x7F000000))); Assert.That(shadow.OffsetX, Is.EqualTo(5)); Assert.That(shadow.OffsetY, Is.EqualTo(10)); Assert.That(shadow.Blur, Is.EqualTo(15)); Assert.That(shadow.Spread, Is.EqualTo(20)); } [Test] public void ShadowStyleCannotBeNull() { var exception = Assert.Throws(() => { EmptyContainer.Create().Shadow(null); }); Assert.That(exception.Message, Does.Contain("The box shadow style cannot be null.")); } [TestCase(-10)] [TestCase(-Size.Epsilon)] public void ShadowBlurCannotBeNegative(float blur) { var exception = Assert.Throws(() => { EmptyContainer.Create().Shadow(new BoxShadowStyle { Blur = blur }); }); Assert.That(exception.Message, Does.Contain("Shadow blur radius cannot be negative.")); } #endregion } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/ElementMock.cs ================================================ using System; using QuestPDF.Drawing; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine { internal sealed class ElementMock : Element { public string Id { get; set; } public Func MeasureFunc { get; set; } public Action DrawFunc { get; set; } internal override SpacePlan Measure(Size availableSpace) => MeasureFunc(availableSpace); internal override void Draw(Size availableSpace) => DrawFunc(availableSpace); } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs ================================================ using System; using QuestPDF.Drawing; using QuestPDF.Infrastructure; using QuestPDF.Skia; using QuestPDF.Skia.Text; using SkiaSharp; namespace QuestPDF.UnitTests.TestEngine { internal sealed class MockCanvas : IDrawingCanvas { public Action TranslateFunc { get; set; } public Action RotateFunc { get; set; } public Action ScaleFunc { get; set; } public Action DrawImageFunc { get; set; } public Action DrawRectFunc { get; set; } public DocumentPageSnapshot GetSnapshot() => throw new NotImplementedException(); public void DrawSnapshot(DocumentPageSnapshot snapshot) => throw new NotImplementedException(); public void Save() => throw new NotImplementedException(); public void Restore() => throw new NotImplementedException(); public void SetZIndex(int index) => throw new NotImplementedException(); public int GetZIndex() => throw new NotImplementedException(); public SkCanvasMatrix GetCurrentMatrix() => throw new NotImplementedException(); public void SetMatrix(SkCanvasMatrix matrix) => throw new NotImplementedException(); public void Translate(Position vector) => TranslateFunc(vector); public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY); public void Rotate(float angle) => RotateFunc(angle); public void DrawLine(Position start, Position end, SkPaint paint) => throw new NotImplementedException(); public void DrawRectangle(Position vector, Size size, SkPaint paint) => throw new NotImplementedException(); public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint) => throw new NotImplementedException(); public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow) => throw new NotImplementedException(); public void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo) => throw new NotImplementedException(); public void DrawImage(SkImage image, Size size) => DrawImageFunc(image, Position.Zero, size); public void DrawPicture(SkPicture picture) => throw new NotImplementedException(); public void DrawSvgPath(string path, Color color) => throw new NotImplementedException(); public void DrawSvg(SkSvgImage svgImage, Size size) => throw new NotImplementedException(); public void DrawOverflowArea(SkRect area) => throw new NotImplementedException(); public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace) => throw new NotImplementedException(); public void ClipRectangle(SkRect clipArea) => throw new NotImplementedException(); public void ClipRoundedRectangle(SkRoundedRect clipArea) => throw new NotImplementedException(); public void DrawHyperlink(Size size, string url, string? description) => throw new NotImplementedException(); public void DrawSectionLink(Size size, string sectionName, string? description) => throw new NotImplementedException(); public void DrawSection(string sectionName) => throw new NotImplementedException(); public int GetSemanticNodeId() => throw new NotImplementedException(); public void SetSemanticNodeId(int nodeId) => throw new NotImplementedException(); } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/OperationBase.cs ================================================ namespace QuestPDF.UnitTests.TestEngine { public abstract class OperationBase { } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs ================================================ using System; using System.Collections.Generic; using QuestPDF.Drawing; using QuestPDF.Infrastructure; using QuestPDF.Skia; using QuestPDF.Skia.Text; using QuestPDF.UnitTests.TestEngine.Operations; namespace QuestPDF.UnitTests.TestEngine { internal sealed class OperationRecordingCanvas : IDrawingCanvas { public ICollection Operations { get; } = new List(); public DocumentPageSnapshot GetSnapshot() => throw new NotImplementedException(); public void DrawSnapshot(DocumentPageSnapshot snapshot) => throw new NotImplementedException(); public void Save() => throw new NotImplementedException(); public void Restore() => throw new NotImplementedException(); public void SetZIndex(int index) => throw new NotImplementedException(); public int GetZIndex() => throw new NotImplementedException(); public SkCanvasMatrix GetCurrentMatrix() => throw new NotImplementedException(); public void SetMatrix(SkCanvasMatrix matrix) => throw new NotImplementedException(); public void Translate(Position vector) => Operations.Add(new CanvasTranslateOperation(vector)); public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY)); public void Rotate(float angle) => Operations.Add(new CanvasRotateOperation(angle)); public void DrawLine(Position start, Position end, SkPaint paint) => throw new NotImplementedException(); public void DrawRectangle(Position vector, Size size, SkPaint paint) => throw new NotImplementedException(); public void DrawComplexBorder(SkRoundedRect innerRect, SkRoundedRect outerRect, SkPaint paint) => throw new NotImplementedException(); public void DrawShadow(SkRoundedRect shadowRect, SkBoxShadow shadow) => throw new NotImplementedException(); public void DrawParagraph(SkParagraph paragraph, int lineFrom, int lineTo) => throw new NotImplementedException(); public void DrawImage(SkImage image, Size size) => Operations.Add(new CanvasDrawImageOperation(Position.Zero, size)); public void DrawPicture(SkPicture picture) => throw new NotImplementedException(); public void DrawSvgPath(string path, Color color) => throw new NotImplementedException(); public void DrawSvg(SkSvgImage svgImage, Size size) => throw new NotImplementedException(); public void DrawOverflowArea(SkRect area) => throw new NotImplementedException(); public void ClipOverflowArea(SkRect availableSpace, SkRect requiredSpace) => throw new NotImplementedException(); public void ClipRectangle(SkRect clipArea) => throw new NotImplementedException(); public void ClipRoundedRectangle(SkRoundedRect clipArea) => throw new NotImplementedException(); public void DrawHyperlink(Size size, string url, string? description) => throw new NotImplementedException(); public void DrawSectionLink(Size size, string sectionName, string? description) => throw new NotImplementedException(); public void DrawSection(string sectionName) => throw new NotImplementedException(); public int GetSemanticNodeId() => throw new NotImplementedException(); public void SetSemanticNodeId(int nodeId) => throw new NotImplementedException(); } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawImageOperation.cs ================================================ using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine.Operations { internal sealed class CanvasDrawImageOperation : OperationBase { public Position Position { get; } public Size Size { get; } public CanvasDrawImageOperation(Position position, Size size) { Position = position; Size = size; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs ================================================ using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine.Operations { internal sealed class CanvasDrawRectangleOperation : OperationBase { public Position Position { get; } public Size Size { get; } public Color Color { get; } public CanvasDrawRectangleOperation(Position position, Size size, Color color) { Position = position; Size = size; Color = color; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawTextOperation.cs ================================================ using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine.Operations { internal sealed class CanvasDrawTextOperation : OperationBase { public string Text { get; } public Position Position { get; } public TextStyle Style { get; } public CanvasDrawTextOperation(string text, Position position, TextStyle style) { Text = text; Position = position; Style = style; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/CanvasRotateOperation.cs ================================================ namespace QuestPDF.UnitTests.TestEngine.Operations { public class CanvasRotateOperation : OperationBase { public float Angle { get; } public CanvasRotateOperation(float angle) { Angle = angle; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/CanvasScaleOperation.cs ================================================ namespace QuestPDF.UnitTests.TestEngine.Operations { public class CanvasScaleOperation : OperationBase { public float ScaleX { get; } public float ScaleY { get; } public CanvasScaleOperation(float scaleX, float scaleY) { ScaleX = scaleX; ScaleY = scaleY; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/CanvasTranslateOperation.cs ================================================ using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine.Operations { internal sealed class CanvasTranslateOperation : OperationBase { public Position Position { get; } public CanvasTranslateOperation(Position position) { Position = position; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/ChildDrawOperation.cs ================================================ using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine.Operations { public class ChildDrawOperation : OperationBase { public string ChildId { get; } public Size Input { get; } public ChildDrawOperation(string childId, Size input) { ChildId = childId; Input = input; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/ChildMeasureOperation.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine.Operations { internal sealed class ChildMeasureOperation : OperationBase { public string ChildId { get; } public Size Input { get; } public SpacePlan Output { get; } public ChildMeasureOperation(string childId, Size input, SpacePlan output) { ChildId = childId; Input = input; Output = output; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/Operations/ElementMeasureOperation.cs ================================================ using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine.Operations { public class ElementMeasureOperation : OperationBase { public ElementMeasureOperation(Size input) { } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/SimpleContainerTests.cs ================================================ using QuestPDF.Drawing; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests.TestEngine { internal static class SimpleContainerTests { #region measure public static void Measure() where TElement : Element, IContainer, new() { Measure_Wrap(); Measure_PartialRender(); Measure_FullRender(); } private static void Measure_Wrap() where TElement : Element, IContainer, new() { TestPlan .For(x => new TElement { Child = x.CreateChild() }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(new Size(400, 300), SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("Forwarded from child")); } private static void Measure_PartialRender() where TElement : Element, IContainer, new() { TestPlan .For(x => new TElement { Child = x.CreateChild() }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(new Size(400, 300), SpacePlan.PartialRender(200, 100)) .CheckMeasureResult(SpacePlan.PartialRender(200, 100)); } private static void Measure_FullRender() where TElement : Element, IContainer, new() { TestPlan .For(x => new TElement { Child = x.CreateChild() }) .MeasureElement(new Size(400, 300)) .ExpectChildMeasure(new Size(400, 300), SpacePlan.FullRender(250, 150)) .CheckMeasureResult(SpacePlan.FullRender(250, 150)); } #endregion public static void Draw() where TElement : Element, IContainer, new() { TestPlan .For(x => new TElement { Child = x.CreateChild() }) .DrawElement(new Size(1200, 900)) .ExpectChildDraw(new Size(1200, 900)) .CheckDrawResult(); } } } ================================================ FILE: Source/QuestPDF.UnitTests/TestEngine/TestPlan.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using NUnit.Framework; using NUnit.Framework.Legacy; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine.Operations; namespace QuestPDF.UnitTests.TestEngine { internal sealed class TestPlan { private const string DefaultChildName = "child"; private static Random Random { get; } = new Random(); private Element Element { get; set; } private IDrawingCanvas Canvas { get; } private Size OperationInput { get; set; } private Queue Operations { get; } = new Queue(); public TestPlan() { Canvas = CreateCanvas(); } public static TestPlan For(Func create) { var plan = new TestPlan(); plan.Element = create(plan); return plan; } private T GetExpected() where T : OperationBase { if (Operations.TryDequeue(out var value) && value is T result) return result; var gotType = value?.GetType()?.Name ?? "null"; Assert.Fail($"Expected: {typeof(T).Name}, got {gotType}: {JsonSerializer.Serialize(value)}"); return null; } private IDrawingCanvas CreateCanvas() { return new MockCanvas { TranslateFunc = position => { var expected = GetExpected(); ClassicAssert.AreEqual(expected.Position.X, position.X, "Translate X"); ClassicAssert.AreEqual(expected.Position.Y, position.Y, "Translate Y"); }, RotateFunc = angle => { var expected = GetExpected(); ClassicAssert.AreEqual(expected.Angle, angle, "Rotate angle"); }, ScaleFunc = (scaleX, scaleY) => { var expected = GetExpected(); ClassicAssert.AreEqual(expected.ScaleX, scaleX, "Scale X"); ClassicAssert.AreEqual(expected.ScaleY, scaleY, "Scale Y"); }, DrawRectFunc = (position, size, color) => { var expected = GetExpected(); ClassicAssert.AreEqual(expected.Position.X, position.X, "Draw rectangle: X"); ClassicAssert.AreEqual(expected.Position.Y, position.Y, "Draw rectangle: Y"); ClassicAssert.AreEqual(expected.Size.Width, size.Width, "Draw rectangle: width"); ClassicAssert.AreEqual(expected.Size.Height, size.Height, "Draw rectangle: height"); ClassicAssert.AreEqual(expected.Color, color, "Draw rectangle: color"); }, DrawImageFunc = (image, position, size) => { var expected = GetExpected(); ClassicAssert.AreEqual(expected.Position.X, position.X, "Draw image: X"); ClassicAssert.AreEqual(expected.Position.Y, position.Y, "Draw image: Y"); ClassicAssert.AreEqual(expected.Size.Width, size.Width, "Draw image: width"); ClassicAssert.AreEqual(expected.Size.Height, size.Height, "Draw image: height"); } }; } public Element CreateChild() => CreateChild(DefaultChildName); public Element CreateChild(string id) { return new ElementMock { Id = id, MeasureFunc = availableSpace => { var expected = GetExpected(); ClassicAssert.AreEqual(expected.ChildId, id); ClassicAssert.AreEqual(expected.Input.Width, availableSpace.Width, $"Measure: width of child '{expected.ChildId}'"); ClassicAssert.AreEqual(expected.Input.Height, availableSpace.Height, $"Measure: height of child '{expected.ChildId}'"); return expected.Output; }, DrawFunc = availableSpace => { var expected = GetExpected(); ClassicAssert.AreEqual(expected.ChildId, id); ClassicAssert.AreEqual(expected.Input.Width, availableSpace.Width, $"Draw: width of child '{expected.ChildId}'"); ClassicAssert.AreEqual(expected.Input.Height, availableSpace.Height, $"Draw: width of child '{expected.ChildId}'"); } }; } public TestPlan MeasureElement(Size input) { OperationInput = input; return this; } public TestPlan DrawElement(Size input) { OperationInput = input; return this; } private TestPlan AddOperation(OperationBase operationBase) { Operations.Enqueue(operationBase); return this; } public TestPlan ExpectChildMeasure(Size expectedInput, SpacePlan returns) { return ExpectChildMeasure(DefaultChildName, expectedInput, returns); } public TestPlan ExpectChildMeasure(string child, Size expectedInput, SpacePlan returns) { return AddOperation(new ChildMeasureOperation(child, expectedInput, returns)); } public TestPlan ExpectChildDraw(Size expectedInput) { return ExpectChildDraw(DefaultChildName, expectedInput); } public TestPlan ExpectChildDraw(string child, Size expectedInput) { return AddOperation(new ChildDrawOperation(child, expectedInput)); } public TestPlan ExpectCanvasTranslate(Position position) { return AddOperation(new CanvasTranslateOperation(position)); } public TestPlan ExpectCanvasTranslate(float left, float top) { return AddOperation(new CanvasTranslateOperation(new Position(left, top))); } public TestPlan ExpectCanvasScale(float scaleX, float scaleY) { return AddOperation(new CanvasScaleOperation(scaleX, scaleY)); } public TestPlan ExpectCanvasRotate(float angle) { return AddOperation(new CanvasRotateOperation(angle)); } public TestPlan ExpectCanvasDrawRectangle(Position position, Size size, Color color) { return AddOperation(new CanvasDrawRectangleOperation(position, size, color)); } public TestPlan ExpectCanvasDrawImage(Position position, Size size) { return AddOperation(new CanvasDrawImageOperation(position, size)); } public TestPlan CheckMeasureResult(SpacePlan expected) { Element.InjectDependencies(null, Canvas); var actual = Element.Measure(OperationInput); Element.ReleaseDisposableChildren(); ClassicAssert.AreEqual(expected.GetType(), actual.GetType()); ClassicAssert.AreEqual(expected.Width, actual.Width, "Measure: width"); ClassicAssert.AreEqual(expected.Height, actual.Height, "Measure: height"); ClassicAssert.AreEqual(expected.Type, actual.Type, "Measure: height"); ClassicAssert.AreEqual(expected.WrapReason, actual.WrapReason, "Measure: wrap"); return this; } public TestPlan CheckDrawResult() { Element.InjectDependencies(null, Canvas); Element.Draw(OperationInput); Element.ReleaseDisposableChildren(); return this; } public TestPlan CheckState(Func condition) { ClassicAssert.IsTrue(condition(Element), "Checking condition"); return this; } public TestPlan CheckState(Func condition) where T : Element { ClassicAssert.IsTrue(Element is T); ClassicAssert.IsTrue(condition(Element as T), "Checking condition"); return this; } public static Element CreateUniqueElement() { var content = new Container(); content .AspectRatio(4 / 3f) .Image(Placeholders.Image); return content; } } } ================================================ FILE: Source/QuestPDF.UnitTests/TextSpanTests.cs ================================================ using System; using System.Linq; using NUnit.Framework; using QuestPDF.Elements.Text; using QuestPDF.Elements.Text.Items; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using QuestPDF.Skia.Text; namespace QuestPDF.UnitTests; public class TextSpanTests { internal (TextSpanDescriptor, TextBlockSpan) CreateTextBlockSpan() { var container = EmptyContainer.Create(); var descriptor = container.Text("test"); var textElement = container.Child as TextBlock; var textBlockSpan = textElement?.Items.SingleOrDefault() as TextBlockSpan; return (descriptor, textBlockSpan); } #region Override Style [Test] public void OverridesStyle() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); var customStyle = TextStyle .Default .FontColor(Colors.Black) .BackgroundColor(Colors.Transparent) .Underline() .DecorationColor(Colors.Red.Medium) .DecorationWavy(); descriptor .FontSize(30) .FontColor(Colors.Blue.Darken4) .BackgroundColor(Colors.Blue.Lighten5) .Bold() .Style(customStyle); Assert.That(textBlockSpan.Style.Size, Is.EqualTo(30)); Assert.That(textBlockSpan.Style.Color, Is.EqualTo(Colors.Black)); Assert.That(textBlockSpan.Style.BackgroundColor, Is.EqualTo(Colors.Transparent)); Assert.That(textBlockSpan.Style.HasUnderline, Is.True); Assert.That(textBlockSpan.Style.DecorationColor, Is.EqualTo(Colors.Red.Medium)); Assert.That(textBlockSpan.Style.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Wavy)); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.Bold)); } [Test] public void OverridesStyle_AcceptsNull() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor .FontSize(30) .FontColor(Colors.Blue.Darken4) .BackgroundColor(Colors.Blue.Lighten5) .Bold() .Style(null); Assert.That(textBlockSpan.Style.Size, Is.EqualTo(30)); Assert.That(textBlockSpan.Style.Color, Is.EqualTo(Colors.Blue.Darken4)); Assert.That(textBlockSpan.Style.BackgroundColor, Is.EqualTo(Colors.Blue.Lighten5)); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.Bold)); } #endregion #region Font Color [Test] public void SetsCorrectFontColor() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.FontColor(Colors.Blue.Medium); Assert.That(textBlockSpan.Style.Color, Is.EqualTo(Colors.Blue.Medium)); } [Test] public void FontColor_AlsoSetsDecorationColor() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.FontColor(Colors.Green.Darken2); Assert.That(textBlockSpan.Style.DecorationColor, Is.EqualTo(Colors.Green.Darken2)); } #endregion #region Background Color [Test] public void SetsCorrectBackgroundColor() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.BackgroundColor(Colors.Yellow.Lighten3); Assert.That(textBlockSpan.Style.BackgroundColor, Is.EqualTo(Colors.Yellow.Lighten3)); } #endregion #region Font Family [Test] public void SetsCorrectFontFamily_Single() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.FontFamily("Arial"); Assert.That(textBlockSpan.Style.FontFamilies, Is.EqualTo(new[] { "Arial" })); } [Test] public void SetsCorrectFontFamily_Multiple() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.FontFamily("Helvetica", "Arial", "sans-serif"); Assert.That(textBlockSpan.Style.FontFamilies, Is.EqualTo(new[] { "Helvetica", "Arial", "sans-serif" })); } [Test] public void FontFamily_EmptyArray_ReturnsUnchanged() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.FontFamily([]); Assert.That(textBlockSpan.Style.FontFamilies, Is.Null); } [Test] public void FontFamily_Null_ReturnsUnchanged() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.FontFamily(null); Assert.That(textBlockSpan.Style.FontFamilies, Is.Null); } #endregion #region Font Size [Test] public void SetsCorrectFontSize() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.FontSize(18); Assert.That(textBlockSpan.Style.Size, Is.EqualTo(18)); } [TestCase(-10)] [TestCase(-5)] [TestCase(-float.Epsilon)] [TestCase(0)] public void FontSize_MustBePositive(float fontSize) { var exception = Assert.Throws(() => { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.FontSize(fontSize); }); Assert.That(exception.Message, Is.EqualTo("Font size must be greater than 0.")); } #endregion #region Line Height [Test] public void SetsCorrectLineHeight() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.LineHeight(1.5f); Assert.That(textBlockSpan.Style.LineHeight, Is.EqualTo(1.5f)); } [Test] public void LineHeight_Null_SetsToNormalLineHeight() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.LineHeight(null); Assert.That(textBlockSpan.Style.LineHeight, Is.EqualTo(TextStyle.NormalLineHeightCalculatedFromFontMetrics)); } [TestCase(-5)] [TestCase(-float.Epsilon)] public void LineHeightMustBePositive(float lineHeight) { var exception = Assert.Throws(() => { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.LineHeight(lineHeight); }); Assert.That(exception.Message, Is.EqualTo("Line height must be greater than 0.")); } [Test] public void LineHeight_AllowsZero() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.LineHeight(0); Assert.That(textBlockSpan.Style.LineHeight, Is.Zero); } #endregion #region Letter Spacing [Test] public void SetsCorrectLetterSpacing_Positive() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.LetterSpacing(0.5f); Assert.That(textBlockSpan.Style.LetterSpacing, Is.EqualTo(0.5f)); } [Test] public void SetsCorrectLetterSpacing_Negative() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.LetterSpacing(-0.2f); Assert.That(textBlockSpan.Style.LetterSpacing, Is.EqualTo(-0.2f)); } [Test] public void LetterSpacing_DefaultParameterIsZero() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.LetterSpacing(); Assert.That(textBlockSpan.Style.LetterSpacing, Is.Zero); } #endregion #region Word Spacing [Test] public void SetsCorrectWordSpacing_Positive() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.WordSpacing(2.0f); Assert.That(textBlockSpan.Style.WordSpacing, Is.EqualTo(2.0f)); } [Test] public void SetsCorrectWordSpacing_Negative() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.WordSpacing(-1.0f); Assert.That(textBlockSpan.Style.WordSpacing, Is.EqualTo(-1.0f)); } [Test] public void WordSpacing_DefaultParameterIsZero() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.WordSpacing(); Assert.That(textBlockSpan.Style.WordSpacing, Is.Zero); } #endregion #region Italic [Test] public void SetsCorrectItalic_Enabled() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Italic(); Assert.That(textBlockSpan.Style.IsItalic, Is.True); } [Test] public void SetsCorrectItalic_Disabled() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Italic().Italic(false); Assert.That(textBlockSpan.Style.IsItalic, Is.False); } #endregion #region Text Decoration [Test] public void SetsCorrectTextDecoration_Strikethrough() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Strikethrough(); Assert.That(textBlockSpan.Style.HasStrikethrough, Is.True); } [Test] public void SetsCorrectTextDecoration_StrikethroughDisabled() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Strikethrough(false); Assert.That(textBlockSpan.Style.HasStrikethrough, Is.False); } [Test] public void SetsCorrectTextDecoration_Underline() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Underline(); Assert.That(textBlockSpan.Style.HasUnderline, Is.True); } [Test] public void SetsCorrectTextDecoration_UnderlineDisabled() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Underline(false); Assert.That(textBlockSpan.Style.HasUnderline, Is.False); } [Test] public void SetsCorrectTextDecoration_Overline() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Overline(); Assert.That(textBlockSpan.Style.HasOverline, Is.True); } [Test] public void SetsCorrectTextDecoration_OverlineDisabled() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Overline(false); Assert.That(textBlockSpan.Style.HasOverline, Is.False); } [Test] public void SetsCorrectTextDecoration_DecorationColor() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DecorationColor(Colors.Red.Medium); Assert.That(textBlockSpan.Style.DecorationColor, Is.EqualTo(Colors.Red.Medium)); } [Test] public void SetsCorrectTextDecoration_DecorationThickness() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DecorationThickness(1.5f); Assert.That(textBlockSpan.Style.DecorationThickness, Is.EqualTo(1.5f)); } [Test] public void SetsCorrectTextDecoration_DecorationSolid() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DecorationSolid(); Assert.That(textBlockSpan.Style.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Solid)); } [Test] public void SetsCorrectTextDecoration_DecorationDouble() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DecorationDouble(); Assert.That(textBlockSpan.Style.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Double)); } [Test] public void SetsCorrectTextDecoration_DecorationWavy() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DecorationWavy(); Assert.That(textBlockSpan.Style.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Wavy)); } [Test] public void SetsCorrectTextDecoration_DecorationDotted() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DecorationDotted(); Assert.That(textBlockSpan.Style.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Dotted)); } [Test] public void SetsCorrectTextDecoration_DecorationDashed() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DecorationDashed(); Assert.That(textBlockSpan.Style.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Dashed)); } #endregion #region Font Weight [Test] public void SetsCorrectSetsCorrectFontWeight_Thin() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Thin(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.Thin)); } [Test] public void SetsCorrectFontWeight_ExtraLight() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.ExtraLight(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.ExtraLight)); } [Test] public void SetsCorrectFontWeight_Light() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Light(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.Light)); } [Test] public void SetsCorrectFontWeight_Normal() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.NormalWeight(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.Normal)); } [Test] public void SetsCorrectFontWeight_Medium() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Medium(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.Medium)); } [Test] public void SetsCorrectFontWeight_SemiBold() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.SemiBold(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.SemiBold)); } [Test] public void SetsCorrectFontWeight_Bold() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Bold(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.Bold)); } [Test] public void SetsCorrectFontWeight_ExtraBold() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.ExtraBold(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.ExtraBold)); } [Test] public void SetsCorrectFontWeight_Black() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Black(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.Black)); } [Test] public void SetsCorrectFontWeight_ExtraBlack() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.ExtraBlack(); Assert.That(textBlockSpan.Style.FontWeight, Is.EqualTo(FontWeight.ExtraBlack)); } #endregion #region Text Position [Test] public void SetsCorrectTextPosition_Subscript() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Subscript(); Assert.That(textBlockSpan.Style.FontPosition, Is.EqualTo(FontPosition.Subscript)); } [Test] public void SetsCorrectTextPosition_Normal() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Superscript().NormalPosition(); // first change from default, then normal Assert.That(textBlockSpan.Style.FontPosition, Is.EqualTo(FontPosition.Normal)); } [Test] public void SetsCorrectTextPosition_Superscript() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.Superscript(); Assert.That(textBlockSpan.Style.FontPosition, Is.EqualTo(FontPosition.Superscript)); } #endregion #region Text Direction [Test] public void SetsCorrectTextDirection_LeftToRight() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DirectionFromLeftToRight(); Assert.That(textBlockSpan.Style.Direction, Is.EqualTo(TextDirection.LeftToRight)); } [Test] public void SetsCorrectTextDirection_RightToLeft() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DirectionFromRightToLeft(); Assert.That(textBlockSpan.Style.Direction, Is.EqualTo(TextDirection.RightToLeft)); } [Test] public void SetsCorrectTextDirection_Auto() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DirectionFromRightToLeft().DirectionAuto(); Assert.That(textBlockSpan.Style.Direction, Is.EqualTo(TextDirection.Auto)); } #endregion #region Font Features [Test] public void EnableFontFeature_SingleFeature() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.EnableFontFeature(FontFeatures.StandardLigatures); Assert.That(textBlockSpan.Style.FontFeatures, Has.Length.EqualTo(1)); Assert.That(textBlockSpan.Style.FontFeatures[0], Is.EqualTo((FontFeatures.StandardLigatures, true))); } [Test] public void DisableFontFeature_SingleFeature() { var (descriptor, textBlockSpan) = CreateTextBlockSpan(); descriptor.DisableFontFeature(FontFeatures.Kerning); Assert.That(textBlockSpan.Style.FontFeatures, Has.Length.EqualTo(1)); Assert.That(textBlockSpan.Style.FontFeatures[0], Is.EqualTo((FontFeatures.Kerning, false))); } [Test] public void FontFeatures_MixedEnableDisable() { var textStyle = TextStyle.Default .EnableFontFeature(FontFeatures.StandardLigatures) .DisableFontFeature(FontFeatures.Kerning) .DisableFontFeature(FontFeatures.DiscretionaryLigatures) .EnableFontFeature(FontFeatures.Kerning); Assert.That(textStyle.FontFeatures, Has.Length.EqualTo(3)); Assert.That(textStyle.FontFeatures[0], Is.EqualTo((FontFeatures.Kerning, true))); Assert.That(textStyle.FontFeatures[1], Is.EqualTo((FontFeatures.DiscretionaryLigatures, false))); Assert.That(textStyle.FontFeatures[2], Is.EqualTo((FontFeatures.StandardLigatures, true))); } #endregion } ================================================ FILE: Source/QuestPDF.UnitTests/TextStyleTests.cs ================================================ using System; using System.Collections.Generic; using NUnit.Framework; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using QuestPDF.Skia.Text; namespace QuestPDF.UnitTests { [TestFixture] public class TextStyleTests { #region Font Color [Test] public void FontColor_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.Color, Is.EqualTo(Colors.Black)); } [Test] public void SetsCorrectFontColor() { var textStyle = TextStyle.Default.FontColor(Colors.Blue.Medium); Assert.That(textStyle.Color, Is.EqualTo(Colors.Blue.Medium)); } [Test] public void FontColor_AlsoSetsDecorationColor() { var textStyle = TextStyle.Default.FontColor(Colors.Green.Darken2); Assert.That(textStyle.DecorationColor, Is.EqualTo(Colors.Green.Darken2)); } #endregion #region Background Color [Test] public void BackgroundColor_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.BackgroundColor, Is.EqualTo(Colors.Transparent)); } [Test] public void SetsCorrectBackgroundColor() { var textStyle = TextStyle.Default.BackgroundColor(Colors.Yellow.Lighten3); Assert.That(textStyle.BackgroundColor, Is.EqualTo(Colors.Yellow.Lighten3)); } #endregion #region Font Family [Test] public void FontFamily_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.FontFamilies, Is.EqualTo(new[] { "Lato" })); } [Test] public void SetsCorrectFontFamily_Single() { var textStyle = TextStyle.Default.FontFamily("Arial"); Assert.That(textStyle.FontFamilies, Is.EqualTo(new[] { "Arial" })); } [Test] public void SetsCorrectFontFamily_Multiple() { var textStyle = TextStyle.Default.FontFamily("Helvetica", "Arial", "sans-serif"); Assert.That(textStyle.FontFamilies, Is.EqualTo(new[] { "Helvetica", "Arial", "sans-serif" })); } [Test] public void FontFamily_EmptyArray_ReturnsUnchanged() { var textStyle = TextStyle.Default.FontFamily([]); Assert.That(textStyle.FontFamilies, Is.Null); } [Test] public void FontFamily_Null_ReturnsUnchanged() { var textStyle = TextStyle.Default.FontFamily(null); Assert.That(textStyle.FontFamilies, Is.Null); } #endregion #region Font Size [Test] public void FontSize_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.Size, Is.EqualTo(12)); } [Test] public void SetsCorrectFontSize() { var textStyle = TextStyle.Default.FontSize(18); Assert.That(textStyle.Size, Is.EqualTo(18)); } [TestCase(-10)] [TestCase(-5)] [TestCase(-float.Epsilon)] [TestCase(0)] public void FontSize_MustBePositive(float fontSize) { var exception = Assert.Throws(() => { TextStyle.Default.FontSize(fontSize); }); Assert.That(exception.Message, Is.EqualTo("Font size must be greater than 0.")); } #endregion #region Line Height [Test] public void LineHeight_Default() { // special value: 0 = normal line height calculated from font metrics var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.LineHeight, Is.Zero); } [Test] public void SetsCorrectLineHeight() { var textStyle = TextStyle.Default.LineHeight(1.5f); Assert.That(textStyle.LineHeight, Is.EqualTo(1.5f)); } [Test] public void LineHeight_Null_SetsToNormalLineHeight() { var textStyle = TextStyle.Default.LineHeight(2.0f).LineHeight(null); Assert.That(textStyle.LineHeight, Is.EqualTo(TextStyle.NormalLineHeightCalculatedFromFontMetrics)); } [TestCase(-5)] [TestCase(-float.Epsilon)] public void LineHeightMustBePositive(float lineHeight) { var exception = Assert.Throws(() => { TextStyle.Default.LineHeight(lineHeight); }); Assert.That(exception.Message, Is.EqualTo("Line height must be greater than 0.")); } [Test] public void LineHeight_AllowsZero() { var textStyle = TextStyle.Default.LineHeight(0); Assert.That(textStyle.LineHeight, Is.Zero); } #endregion #region Letter Spacing [Test] public void LetterSpacing_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.LetterSpacing, Is.Zero); } [Test] public void SetsCorrectLetterSpacing_Positive() { var textStyle = TextStyle.Default.LetterSpacing(0.5f); Assert.That(textStyle.LetterSpacing, Is.EqualTo(0.5f)); } [Test] public void SetsCorrectLetterSpacing_Negative() { var textStyle = TextStyle.Default.LetterSpacing(-0.2f); Assert.That(textStyle.LetterSpacing, Is.EqualTo(-0.2f)); } [Test] public void LetterSpacing_DefaultParameterIsZero() { var textStyle = TextStyle.Default.LetterSpacing(); Assert.That(textStyle.LetterSpacing, Is.Zero); } #endregion #region Word Spacing [Test] public void WordSpacing_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.WordSpacing, Is.Zero); } [Test] public void SetsCorrectWordSpacing_Positive() { var textStyle = TextStyle.Default.WordSpacing(2.0f); Assert.That(textStyle.WordSpacing, Is.EqualTo(2.0f)); } [Test] public void SetsCorrectWordSpacing_Negative() { var textStyle = TextStyle.Default.WordSpacing(-1.0f); Assert.That(textStyle.WordSpacing, Is.EqualTo(-1.0f)); } [Test] public void WordSpacing_DefaultParameterIsZero() { var textStyle = TextStyle.Default.WordSpacing(); Assert.That(textStyle.WordSpacing, Is.Zero); } #endregion #region Italic [Test] public void Italic_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.IsItalic, Is.False); } [Test] public void SetsCorrectItalic_Enabled() { var textStyle = TextStyle.Default.Italic(); Assert.That(textStyle.IsItalic, Is.True); } [Test] public void SetsCorrectItalic_Disabled() { var textStyle = TextStyle.Default.Italic().Italic(false); Assert.That(textStyle.IsItalic, Is.False); } #endregion #region Text Decoration [Test] public void TextDecoration_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.HasStrikethrough, Is.False); Assert.That(textStyle.HasUnderline, Is.False); Assert.That(textStyle.HasOverline, Is.False); Assert.That(textStyle.DecorationColor, Is.EqualTo(Colors.Black)); Assert.That(textStyle.DecorationThickness, Is.EqualTo(1f)); Assert.That(textStyle.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Solid)); } [Test] public void SetsCorrectTextDecoration_Strikethrough() { var textStyle = TextStyle.Default.Strikethrough(); Assert.That(textStyle.HasStrikethrough, Is.True); } [Test] public void SetsCorrectTextDecoration_StrikethroughDisabled() { var textStyle = TextStyle.Default.Strikethrough().Strikethrough(false); Assert.That(textStyle.HasStrikethrough, Is.False); } [Test] public void SetsCorrectTextDecoration_Underline() { var textStyle = TextStyle.Default.Underline(); Assert.That(textStyle.HasUnderline, Is.True); } [Test] public void SetsCorrectTextDecoration_UnderlineDisabled() { var textStyle = TextStyle.Default.Underline().Underline(false); Assert.That(textStyle.HasUnderline, Is.False); } [Test] public void SetsCorrectTextDecoration_Overline() { var textStyle = TextStyle.Default.Overline(); Assert.That(textStyle.HasOverline, Is.True); } [Test] public void SetsCorrectTextDecoration_OverlineDisabled() { var textStyle = TextStyle.Default.Overline().Overline(false); Assert.That(textStyle.HasOverline, Is.False); } [Test] public void SetsCorrectTextDecoration_DecorationColor() { var textStyle = TextStyle.Default.DecorationColor(Colors.Red.Medium); Assert.That(textStyle.DecorationColor, Is.EqualTo(Colors.Red.Medium)); } [Test] public void SetsCorrectTextDecoration_DecorationThickness() { var textStyle = TextStyle.Default.DecorationThickness(1.5f); Assert.That(textStyle.DecorationThickness, Is.EqualTo(1.5f)); } [Test] public void SetsCorrectTextDecoration_DecorationSolid() { var textStyle = TextStyle.Default.DecorationSolid(); Assert.That(textStyle.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Solid)); } [Test] public void SetsCorrectTextDecoration_DecorationDouble() { var textStyle = TextStyle.Default.DecorationDouble(); Assert.That(textStyle.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Double)); } [Test] public void SetsCorrectTextDecoration_DecorationWavy() { var textStyle = TextStyle.Default.DecorationWavy(); Assert.That(textStyle.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Wavy)); } [Test] public void SetsCorrectTextDecoration_DecorationDotted() { var textStyle = TextStyle.Default.DecorationDotted(); Assert.That(textStyle.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Dotted)); } [Test] public void SetsCorrectTextDecoration_DecorationDashed() { var textStyle = TextStyle.Default.DecorationDashed(); Assert.That(textStyle.DecorationStyle, Is.EqualTo(TextStyleConfiguration.TextDecorationStyle.Dashed)); } #endregion #region Font Weight [Test] public void FontWeight_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.Normal)); } [Test] public void SetsCorrectSetsCorrectFontWeight_Thin() { var textStyle = TextStyle.Default.Thin(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.Thin)); } [Test] public void SetsCorrectFontWeight_ExtraLight() { var textStyle = TextStyle.Default.ExtraLight(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.ExtraLight)); } [Test] public void SetsCorrectFontWeight_Light() { var textStyle = TextStyle.Default.Light(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.Light)); } [Test] public void SetsCorrectFontWeight_Normal() { var textStyle = TextStyle.Default.Bold().NormalWeight(); // first change from default, then normal Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.Normal)); } [Test] public void SetsCorrectFontWeight_Medium() { var textStyle = TextStyle.Default.Medium(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.Medium)); } [Test] public void SetsCorrectFontWeight_SemiBold() { var textStyle = TextStyle.Default.SemiBold(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.SemiBold)); } [Test] public void SetsCorrectFontWeight_Bold() { var textStyle = TextStyle.Default.Bold(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.Bold)); } [Test] public void SetsCorrectFontWeight_ExtraBold() { var textStyle = TextStyle.Default.ExtraBold(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.ExtraBold)); } [Test] public void SetsCorrectFontWeight_Black() { var textStyle = TextStyle.Default.Black(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.Black)); } [Test] public void SetsCorrectFontWeight_ExtraBlack() { var textStyle = TextStyle.Default.ExtraBlack(); Assert.That(textStyle.FontWeight, Is.EqualTo(FontWeight.ExtraBlack)); } #endregion #region Text Position [Test] public void TextPosition_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.FontPosition, Is.EqualTo(FontPosition.Normal)); } [Test] public void SetsCorrectTextPosition_Subscript() { var textStyle = TextStyle.Default.Subscript(); Assert.That(textStyle.FontPosition, Is.EqualTo(FontPosition.Subscript)); } [Test] public void SetsCorrectTextPosition_Normal() { var textStyle = TextStyle.Default.Subscript().NormalPosition(); // first change from default, then normal Assert.That(textStyle.FontPosition, Is.EqualTo(FontPosition.Normal)); } [Test] public void SetsCorrectTextPosition_Superscript() { var textStyle = TextStyle.Default.Superscript(); Assert.That(textStyle.FontPosition, Is.EqualTo(FontPosition.Superscript)); } #endregion #region Text Direction [Test] public void TextDirection_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.Direction, Is.EqualTo(TextDirection.Auto)); } [Test] public void SetsCorrectTextDirection_LeftToRight() { var textStyle = TextStyle.Default.DirectionFromLeftToRight(); Assert.That(textStyle.Direction, Is.EqualTo(TextDirection.LeftToRight)); } [Test] public void SetsCorrectTextDirection_RightToLeft() { var textStyle = TextStyle.Default.DirectionFromRightToLeft(); Assert.That(textStyle.Direction, Is.EqualTo(TextDirection.RightToLeft)); } [Test] public void SetsCorrectTextDirection_Auto() { var textStyle = TextStyle.Default.DirectionFromRightToLeft().DirectionAuto(); // first change from default, then auto Assert.That(textStyle.Direction, Is.EqualTo(TextDirection.Auto)); } #endregion #region Font Features [Test] public void FontFeatures_Default() { var textStyle = TextStyle.LibraryDefault; Assert.That(textStyle.FontFeatures, Is.Empty); } [Test] public void EnableFontFeature_SingleFeature() { var textStyle = TextStyle.Default .EnableFontFeature(FontFeatures.StandardLigatures); Assert.That(textStyle.FontFeatures, Has.Length.EqualTo(1)); Assert.That(textStyle.FontFeatures[0], Is.EqualTo((FontFeatures.StandardLigatures, true))); } [Test] public void DisableFontFeature_SingleFeature() { var textStyle = TextStyle.Default .DisableFontFeature(FontFeatures.Kerning); Assert.That(textStyle.FontFeatures, Has.Length.EqualTo(1)); Assert.That(textStyle.FontFeatures[0], Is.EqualTo((FontFeatures.Kerning, false))); } // NOTE: font features applied further down the chain override those applied earlier, and will appear first in the list [Test] public void FontFeatures_MixedEnableDisable() { var textStyle = TextStyle.Default .EnableFontFeature(FontFeatures.StandardLigatures) .DisableFontFeature(FontFeatures.Kerning) .EnableFontFeature(FontFeatures.DiscretionaryLigatures); Assert.That(textStyle.FontFeatures, Has.Length.EqualTo(3)); Assert.That(textStyle.FontFeatures[0], Is.EqualTo((FontFeatures.DiscretionaryLigatures, true))); Assert.That(textStyle.FontFeatures[1], Is.EqualTo((FontFeatures.Kerning, false))); Assert.That(textStyle.FontFeatures[2], Is.EqualTo((FontFeatures.StandardLigatures, true))); } [Test] public void FontFeatures_OverrideSameFeature() { var textStyle = TextStyle.Default .EnableFontFeature(FontFeatures.StandardLigatures) .DisableFontFeature(FontFeatures.Kerning) .DisableFontFeature(FontFeatures.Kerning) .DisableFontFeature(FontFeatures.StandardLigatures); Assert.That(textStyle.FontFeatures, Has.Length.EqualTo(2)); Assert.That(textStyle.FontFeatures[0], Is.EqualTo((FontFeatures.StandardLigatures, false))); Assert.That(textStyle.FontFeatures[1], Is.EqualTo((FontFeatures.Kerning, false))); } #endregion // TODO: add tests for text style inheritance [Test] public void ApplyInheritedAndGlobalStyle() { // arrange var defaultTextStyle = TextStyle .Default .FontSize(20) .FontFamily("Arial", "Microsoft YaHei") .BackgroundColor(Colors.Green.Lighten2) .EnableFontFeature(FontFeatures.StandardLigatures); var spanTextStyle = TextStyle .Default .FontFamily("Times New Roman", "Arial", "Calibri") .Bold() .Strikethrough() .BackgroundColor(Colors.Red.Lighten2) .DisableFontFeature(FontFeatures.StandardLigatures) .EnableFontFeature(FontFeatures.Kerning); // act var targetStyle = spanTextStyle.ApplyInheritedStyle(defaultTextStyle).ApplyGlobalStyle(); // assert var expectedStyle = TextStyle.LibraryDefault with { Id = targetStyle.Id, // expect to break when adding new TextStyle properties, so use the real one Size = 20, FontFamilies = new[] { "Times New Roman", "Arial", "Calibri", "Microsoft YaHei", "Lato" }, FontWeight = FontWeight.Bold, BackgroundColor = Colors.Red.Lighten2, FontFeatures = new[] { (FontFeatures.Kerning, true), (FontFeatures.StandardLigatures, false) }, HasStrikethrough = true }; Assert.That(targetStyle, Is.Not.Null); Assert.That(targetStyle.Id, Is.GreaterThan(1)); Assert.That(targetStyle.ToString(), Is.EqualTo(expectedStyle.ToString())); } } } ================================================ FILE: Source/QuestPDF.UnitTests/TranslateTests.cs ================================================ using System; using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests { [TestFixture] public class TranslateTests { [TestCase(0, 0, "")] [TestCase(5, 0, "X=5")] [TestCase(-10, 0, "X=-10")] [TestCase(0, 15, "Y=15")] [TestCase(0, -20, "Y=-20")] [TestCase(30, -40, "X=30 Y=-40")] [TestCase(1.2345f, -2.3456f, "X=1.2 Y=-2.3")] public void CompanionHint(float x, float y, string expected) { var container = EmptyContainer.Create(); container.TranslateX(x).TranslateY(y); var translationElement = container.Child as Translate; var companionHint = translationElement?.GetCompanionHint(); Assert.That(companionHint, Is.EqualTo(expected)); } [Test] public void HorizontalTranslationIsCumulative() { var container = EmptyContainer.Create(); container.TranslateX(-5).TranslateX(10).TranslateX(20); var rowContainer = container.Child as Translate; Assert.That(rowContainer?.TranslateX, Is.EqualTo(25)); } [Test] public void VerticalTranslationIsCumulative() { var container = EmptyContainer.Create(); container.TranslateY(5).TranslateY(-10).TranslateY(20); var rowContainer = container.Child as Translate; Assert.That(rowContainer?.TranslateY, Is.EqualTo(15)); } [Test] public void HorizontalTranslationSupportsUnitConversion() { var container = EmptyContainer.Create(); container.TranslateX(2, Unit.Inch); var rowContainer = container.Child as Translate; Assert.That(rowContainer?.TranslateX, Is.EqualTo(144)); } [Test] public void VerticalTranslationSupportsUnitConversion() { var container = EmptyContainer.Create(); container.TranslateY(3, Unit.Inch); var rowContainer = container.Child as Translate; Assert.That(rowContainer?.TranslateY, Is.EqualTo(216)); } } } ================================================ FILE: Source/QuestPDF.UnitTests/UnconstrainedTests.cs ================================================ using NUnit.Framework; using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { [TestFixture] public class UnconstrainedTests { #region measure [Test] public void Measure_Wrap() { TestPlan .For(x => new Unconstrained { Child = x.CreateChild() }) .MeasureElement(new Size(900, 800)) .ExpectChildMeasure(Size.Max, SpacePlan.Wrap("Mock")) .CheckMeasureResult(SpacePlan.Wrap("Forwarded from child")); } [Test] public void Measure_PartialRender() { TestPlan .For(x => new Unconstrained { Child = x.CreateChild() }) .MeasureElement(new Size(900, 800)) .ExpectChildMeasure(Size.Max, SpacePlan.PartialRender(1200, 1600)) .CheckMeasureResult(SpacePlan.PartialRender(Size.Zero)); } [Test] public void Measure_FullRender() { TestPlan .For(x => new Unconstrained { Child = x.CreateChild() }) .MeasureElement(new Size(900, 800)) .ExpectChildMeasure(Size.Max, SpacePlan.FullRender(1200, 1600)) .CheckMeasureResult(SpacePlan.FullRender(Size.Zero)); } #endregion #region draw [Test] public void Draw_SkipWhenChildWraps() { TestPlan .For(x => new Unconstrained { Child = x.CreateChild() }) .DrawElement(new Size(900, 800)) .ExpectChildMeasure(Size.Max, SpacePlan.Wrap("Mock")) .CheckDrawResult(); } [Test] public void Draw_WhenChildPartiallyRenders() { TestPlan .For(x => new Unconstrained { Child = x.CreateChild() }) .DrawElement(new Size(900, 800)) .ExpectChildMeasure(Size.Max, SpacePlan.PartialRender(1200, 1600)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw(new Size(1200, 1600)) .ExpectCanvasTranslate(0, 0) .CheckDrawResult(); } [Test] public void Draw_WhenChildFullyRenders() { TestPlan .For(x => new Unconstrained { Child = x.CreateChild() }) .DrawElement(new Size(900, 800)) .ExpectChildMeasure(Size.Max, SpacePlan.FullRender(1600, 1000)) .ExpectCanvasTranslate(0, 0) .ExpectChildDraw(new Size(1600, 1000)) .ExpectCanvasTranslate(0, 0) .CheckDrawResult(); } [Test] public void Draw_WhenChildPartiallyRenders_RightToLeft() { TestPlan .For(x => new Unconstrained { Child = x.CreateChild(), ContentDirection = ContentDirection.RightToLeft }) .DrawElement(new Size(900, 800)) .ExpectChildMeasure(Size.Max, SpacePlan.PartialRender(1200, 1600)) .ExpectCanvasTranslate(-1200, 0) .ExpectChildDraw(new Size(1200, 1600)) .ExpectCanvasTranslate(1200, 0) .CheckDrawResult(); } [Test] public void Draw_WhenChildFullyRenders_RightToLeft() { TestPlan .For(x => new Unconstrained { Child = x.CreateChild(), ContentDirection = ContentDirection.RightToLeft }) .DrawElement(new Size(900, 800)) .ExpectChildMeasure(Size.Max, SpacePlan.FullRender(1600, 1000)) .ExpectCanvasTranslate(-1600, 0) .ExpectChildDraw(new Size(1600, 1000)) .ExpectCanvasTranslate(1600, 0) .CheckDrawResult(); } #endregion } } ================================================ FILE: Source/QuestPDF.UnitTests/UnitConversionTests.cs ================================================ using System.Collections.Generic; using NUnit.Framework; using QuestPDF.Infrastructure; namespace QuestPDF.UnitTests; [TestFixture] public class UnitConversionTests { [TestCase(Unit.Point, 1f, 1f)] [TestCase(Unit.Inch, 1f, 72f)] [TestCase(Unit.Feet, 1f, 864f)] [TestCase(Unit.Mil, 1000f, 72f)] [TestCase(Unit.Centimetre, 2.54f, 72f)] // 2.54 cm = 1 inch [TestCase(Unit.Millimetre, 25.4f, 72f)] // 25.4 mm = 1 inch [TestCase(Unit.Meter, 0.0254f, 72f)] // 0.0254 m = 1 inch public void ToPoints_ConvertsCorrectly(Unit unit, float input, float expected) { var result = input.ToPoints(unit); Assert.That(result, Is.EqualTo(expected)); var result2 = (input * 2).ToPoints(unit); Assert.That(result2, Is.EqualTo(expected * 2)); } } ================================================ FILE: Source/QuestPDF.VisualTests/LineTests.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.VisualTests; public class LineTests { [Test] public void ThicknessHorizontal( [Values(1, 2, 4, 8)] float thickness) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Column(column => { column.Item().Height(50).Background(Colors.Blue.Lighten4); column.Item().LineHorizontal(thickness); column.Item().Height(50).Background(Colors.Green.Lighten4); }); }); } [Test] public void ThicknessVertical( [Values(1, 2, 4, 8)] float thickness) { VisualTest.PerformWithDefaultPageSettings(container => { container .Height(100) .Row(row => { row.AutoItem().Width(50).Background(Colors.Blue.Lighten4); row.AutoItem().LineVertical(thickness); row.AutoItem().Width(50).Background(Colors.Green.Lighten4); }); }); } private static readonly IEnumerable ColorValues = [Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium]; [Test] public void Color( [ValueSource(nameof(ColorValues))] Color color) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .LineHorizontal(4) .LineColor(color); }); } private static readonly IEnumerable GradientValues = [ [Colors.Red.Medium, Colors.Green.Medium], [Colors.Red.Medium, Colors.Yellow.Medium, Colors.Green.Medium], [Colors.Blue.Medium, Colors.LightBlue.Medium, Colors.Cyan.Medium, Colors.Teal.Medium, Colors.Green.Medium] ]; [Test] public void Gradient( [ValueSource(nameof(GradientValues))] Color[] colors) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(400) .LineHorizontal(16) .LineGradient(colors); }); } private static readonly IEnumerable DashPatternCases = [ [1, 1], [1, 2], [2, 1], [2, 2], [4, 4], [8, 8], [4, 4, 12, 4], [4, 4, 8, 8, 12, 12], ]; [Test, TestCaseSource(nameof(DashPatternCases))] public void DashPattern(float[] dashPattern) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(400) .LineHorizontal(4) .LineDashPattern(dashPattern); }); } [Test] public void Complex() { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(300) .LineHorizontal(8) .LineDashPattern([4, 4, 8, 8, 12, 12]) .LineGradient([Colors.Red.Medium, Colors.Orange.Medium, Colors.Yellow.Medium]); }); } #region IStateful [Test] public void LineShouldRenderOnlyOnce() { VisualTest.PerformWithDefaultPageSettings(container => { container .Height(400) .Width(400) .Row(row => { row.RelativeItem().LineHorizontal(10); row.RelativeItem().Column(column => { column.Item().Height(300).Background(Colors.Blue.Lighten1); column.Item().Height(200).Background(Colors.Blue.Lighten3); }); }); }); } [Test] public void LineShouldRerenderWhenCombinedWithRepeat() { VisualTest.PerformWithDefaultPageSettings(container => { container .Height(400) .Width(400) .Row(row => { row.RelativeItem().Repeat().LineHorizontal(10); row.RelativeItem().Column(column => { column.Item().Height(300).Background(Colors.Blue.Lighten1); column.Item().Height(200).Background(Colors.Blue.Lighten3); }); }); }); } #endregion } ================================================ FILE: Source/QuestPDF.VisualTests/QuestPDF.VisualTests.csproj ================================================ net10.0 enable enable true en false true all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive PreserveNewest PreserveNewest ================================================ FILE: Source/QuestPDF.VisualTests/RotateTests.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.VisualTests; public class RotateTests { [Test] public void Rotate( [Values(-15, 0, 30, 45, 60, 90)] int angle) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(300) .Height(300) .AlignCenter() .AlignMiddle() .Unconstrained() .Rotate(angle) // <- .TranslateX(-100) .TranslateY(-50) .Width(200) .Height(100) .Background(Colors.Grey.Lighten3) .AlignCenter() .AlignMiddle() .Text($"Rotation: {angle} deg"); }); } } ================================================ FILE: Source/QuestPDF.VisualTests/SimpleRotateTests.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; namespace QuestPDF.VisualTests; public class SimpleRotateTests { [Test] public void Rotate( [Values(0, 1, 2, 3)] int rotationCount) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(150) .Height(150) .Element(element => { foreach (var i in Enumerable.Range(0, rotationCount)) element = element.RotateRight(); return element; }) .Shrink() .Background(Colors.Grey.Lighten3) .Padding(10) .Text($"Rotation #{rotationCount}"); }); } } ================================================ FILE: Source/QuestPDF.VisualTests/StyledBoxTests.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.VisualTests; public class StyledBoxTests { #region Background private static readonly IEnumerable BackgroundColorValues = [ Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium ]; [Test, TestCaseSource(nameof(BackgroundColorValues))] public void BackgroundColor(Color color) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Background(color); }); } private static readonly IEnumerable BackgroundGradientColorsValues = [ [Colors.Red.Medium, Colors.Green.Darken2], [Colors.Red.Medium, Colors.Yellow.Medium, Colors.Green.Medium], [Colors.Blue.Medium, Colors.LightBlue.Medium, Colors.Cyan.Medium, Colors.Teal.Medium, Colors.Green.Medium] ]; [Test, TestCaseSource(nameof(BackgroundGradientColorsValues))] public void BackgroundGradientColors(Color[] gradientColors) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .BackgroundLinearGradient(0, gradientColors); }); } [Test] public void BackgroundGradientAngle( [Values(0, 30, 45, 60, 90)] float angle) { var gradient = new[] { Colors.Black, Colors.White }; VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .BackgroundLinearGradient(angle, gradient); }); } [Test] public void BackgroundUniformRoundedCorners( [Values(0, 5, 10, 25, 50, 100)] float radius) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Background(Colors.Grey.Medium) .CornerRadius(radius); }); } [TestCase(0, 10, 20, 40)] [TestCase(10, 20, 40, 0)] [TestCase(20, 40, 0, 10)] [TestCase(40, 0, 10, 20)] public void BackgroundRoundedCorners(float topLeft, float topRight, float bottomRight, float bottomLeft) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Background(Colors.Grey.Medium) .CornerRadiusTopLeft(topLeft) .CornerRadiusTopRight(topRight) .CornerRadiusBottomRight(bottomRight) .CornerRadiusBottomLeft(bottomLeft); }); } #endregion #region Border [Test] public void BorderThicknessUniform( [Values(1, 2, 4, 8)] float thickness) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Height(100) .Border(thickness) .BorderColor(Colors.Black) .Background(Colors.Grey.Lighten2); }); } [Test] public void BorderThickness( [Values(0, 2)] float left, [Values(0, 4)] float top, [Values(0, 6)] float right, [Values(0, 8)] float bottom) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Height(100) .BorderLeft(left) .BorderTop(top) .BorderRight(right) .BorderBottom(bottom) .BorderColor(Colors.Black) .Background(Colors.Grey.Lighten2); }); } [Test] public void BorderRoundedCornersUniform( [Values(0, 5, 10, 25, 50, 100)] float radius) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Border(2) .CornerRadius(radius) .BorderColor(Colors.Black) .Background(Colors.Grey.Lighten2); }); } [Test] public void BorderRoundedCornersWithVariousCornerRadius( [Values(0, 5)] float left, [Values(0, 5)] float top, [Values(0, 5)] float right, [Values(0, 5)] float bottom, [Values(5, 15)] float roundedRadius) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Height(50) .BorderLeft(left) .BorderTop(top) .BorderRight(right) .BorderBottom(bottom) .CornerRadius(roundedRadius) .Background(Colors.Blue.Lighten3); }); } public static readonly IEnumerable BorderColorCases = [ Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium ]; [Test, TestCaseSource(nameof(BorderColorCases))] public void BorderColor(Color color) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Border(5) .BorderColor(color) .Background(Colors.Grey.Lighten2); }); } private static readonly IEnumerable BorderGradientColorsValues = [ [Colors.Red.Medium, Colors.Green.Darken2], [Colors.Red.Medium, Colors.Yellow.Medium, Colors.Green.Medium], [Colors.Blue.Medium, Colors.LightBlue.Medium, Colors.Cyan.Medium, Colors.Teal.Medium, Colors.Green.Medium] ]; [Test, TestCaseSource(nameof(BorderGradientColorsValues))] public void BorderGradientColors(Color[] gradientColors) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Border(10) .CornerRadius(5) .BorderLinearGradient(0, gradientColors) .Background(Colors.Grey.Lighten2); }); } [Test] public void BorderGradientAngle( [Values(0, 30, 45, 60, 90)] float angle) { var gradient = new[] { Colors.Black, Colors.White }; VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Border(10) .CornerRadius(5) .BorderLinearGradient(angle, gradient); }); } private void BorderAlignmentTest(Func configuration) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Border(10) .CornerRadius(10) .BorderColor(Colors.Black.WithAlpha(0.5f)) .Apply(configuration) .Background(Colors.Blue.Lighten3); }); } [Test] public void BorderAlignmentInside() { BorderAlignmentTest(x => x.BorderAlignmentInside()); } [Test] public void BorderAlignmentMiddle() { BorderAlignmentTest(x => x.BorderAlignmentMiddle()); } [Test] public void BorderAlignmentOutside() { BorderAlignmentTest(x => x.BorderAlignmentOutside()); } #endregion #region Shadow private static readonly IEnumerable ShadowColorValues = [ Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium ]; [Test, TestCaseSource(nameof(ShadowColorValues))] public void ShadowColor(Color shadowColor) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .Height(100) .Background(Colors.White) .Shadow(new BoxShadowStyle { Color = shadowColor, Blur = 8 }); }); } [Test] public void ShadowBlur( [Values(2, 4, 8, 16)] float blur) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Height(50) .Background(Colors.White) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken2, Blur = blur, }); }); } [Test] public void ShadowWithoutBlur( [Values] bool applyRoundedCorners, [Values] bool applySpread, [Values] bool applyOffset) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Height(50) .CornerRadius(applyRoundedCorners ? 20f : 0f) .Background(Colors.LightBlue.Medium) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken2, OffsetX = applyOffset ? 4f : 0f, OffsetY = applyOffset ? 4f : 0f, Spread = applySpread ? 8f : 0f, }); }); } [Test] public void ShadowSpread( [Values(-4, 0, 4, 8, 12)] float spread) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Height(100) .Background(Colors.White) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken2, Blur = 4, Spread = spread }); }); } [Test] public void ShadowOffsetX( [Values(-8, -4, 0, 4, 8)] float offsetX) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Height(100) .Background(Colors.White) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken2, Blur = 8, OffsetX = offsetX }); }); } [Test] public void ShadowOffsetY( [Values(-8, -4, 0, 4, 8)] float offsetY) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(100) .Height(100) .Background(Colors.White) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken2, Blur = 8, OffsetY = offsetY }); }); } #endregion #region Clipping [Test] public void ClipImage() { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(200) .CornerRadius(25) .Shadow(new BoxShadowStyle { Color = Colors.Grey.Darken2, Blur = 8 }) .Image("Resources/gradient.png"); }); } [Test] public void ClipText() { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(300) .Border(2, Colors.Black) .CornerRadius(75) .Text(Placeholders.LoremIpsum()); }); } [Test] public void ClipContent() { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(400) .Border(2, Colors.Black) .CornerRadius(150) .Table(table => { table.ColumnsDefinition(columns => { foreach (var i in Enumerable.Range(1, 10)) columns.RelativeColumn(); }); foreach (var i in Enumerable.Range(0, 100)) { table.Cell() .Border(1) .Padding(5) .AlignCenter() .Text(i.ToString()); } }); }); } #endregion } ================================================ FILE: Source/QuestPDF.VisualTests/TestsSetup.cs ================================================ using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.VisualTests { [SetUpFixture] public class TestsSetup { [OneTimeSetUp] public static void Setup() { var currentRuntime = NativeDependencyProvider.GetRuntimePlatform(); if (currentRuntime != "osx-arm64") { Assert.Ignore( "Visual tests are performed based on osx-arm64 runtime output. " + "Each operating system (Windows, Linux, macOS) uses different font analysis and text rendering libraries. " + "Moreover, each CPU architecture may produce slightly different floating-point calculation results. " + "This may lead to different results even though the same code and configuration are used. " + "Therefore, visual tests are not expected to pass entirely on other operating systems or CPU architectures."); } QuestPDF.Settings.License = LicenseType.Community; QuestPDF.Settings.UseEnvironmentFonts = false; VisualTestEngine.ClearActualOutputDirectories(); } } } ================================================ FILE: Source/QuestPDF.VisualTests/TextStyleTests.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.VisualTests; public class TextStyleTests { private static readonly IEnumerable FontColor_Values = [ Colors.Red.Darken3, Colors.Green.Darken3, Colors.Blue.Darken3 ]; [Test, TestCaseSource(nameof(FontColor_Values))] public void FontsColor(Color fontColor) { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Text can be displayed in different "); text.Span("font colors").FontColor(fontColor); text.Span("."); }); }); } private static readonly IEnumerable BackgroundColor_Values = [ Colors.Red.Lighten4, Colors.Green.Lighten4, Colors.Blue.Lighten4 ]; [Test, TestCaseSource(nameof(BackgroundColor_Values))] public void BackgroundColor(Color backgroundColor) { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Text can be displayed with different "); text.Span("background colors").BackgroundColor(backgroundColor); text.Span("."); }); }); } [Test] public void FontSize([Values(12, 16, 24)] float fontSize) { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Text can be displayed in different "); text.Span($"font sizes ({fontSize}pt)").FontSize(fontSize); text.Span("."); }); }); } [Test] public void LineHeight([Values(0.75f, 1f, 1.5f)] float lineHeight) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(400) .Text(text => { text.Span($"Line height: {lineHeight}\n\n").Bold().FontColor(Colors.Blue.Darken2); text.Span(Placeholders.LoremIpsum()).LineHeight(lineHeight); }); }); } [Test] public void WordSpacing([Values(-0.1f, 0f, 0.25f, 1f)] float wordSpacing) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(400) .Text(text => { text.Span($"Word spacing: {wordSpacing}\n\n").Bold().FontColor(Colors.Blue.Darken2); text.Span(Placeholders.LoremIpsum()).WordSpacing(wordSpacing); }); }); } [Test] public void LetterSpacing([Values(-0.1f, 0f, 0.1f, 0.25f)] float letterSpacing) { VisualTest.PerformWithDefaultPageSettings(container => { container .Width(400) .Text(text => { text.Span($"Letter spacing: {letterSpacing}\n\n").Bold().FontColor(Colors.Blue.Darken2); text.Span(Placeholders.LoremIpsum()).LetterSpacing(letterSpacing); }); }); } [Test] public void Italic() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("The "); text.Span("italic effect").Italic(); text.Span(" slants the letters slightly to the right."); }); }); } #region Font Decoration [Test] public void FontDecoration_Underline() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Text can be decorated with "); text.Span("an underline").Underline().DecorationThickness(2).DecorationColor(Colors.Red.Medium); text.Span("."); }); }); } [Test] public void FontDecoration_Strikethrough() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Text can be decorated with "); text.Span("a strikeout").Strikethrough().DecorationThickness(2).DecorationColor(Colors.Red.Medium); text.Span("."); }); }); } [Test] public void FontDecoration_Overline() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Text can be decorated with "); text.Span("an overline").Overline().DecorationThickness(2).DecorationColor(Colors.Red.Medium); text.Span("."); }); }); } [Test] public void FontDecoration_Combined() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Text may have "); text.Span("multiple decoration") .Underline() .Strikethrough() .Overline() .DecorationThickness(2) .DecorationColor(Colors.Blue.Medium); text.Span(" applied to it."); }); }); } private static readonly IEnumerable FontDecoration_Color_Values = [ Colors.Red.Medium, Colors.Green.Medium, Colors.Blue.Medium ]; [Test, TestCaseSource(nameof(FontDecoration_Color_Values))] public void FontDecoration_Color(Color decorationColor) { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("The color of "); text.Span("the text decoration").Underline().DecorationDashed().DecorationThickness(2).DecorationColor(decorationColor); text.Span(" can be changed."); }); }); } [Test] public void FontDecoration_Thickness([Values(1f, 2f, 3f)] float thickness) { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("The thickness of "); text.Span("the text decoration").Underline().DecorationWavy().DecorationThickness(thickness).DecorationColor(Colors.Red.Medium); text.Span(" can be changed."); }); }); } [Test] public void FontDecoration_Solid() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a solid-line text decoration").Underline().DecorationSolid(); text.Span("."); }); }); } [Test] public void FontDecoration_Double() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a double-line text decoration").Underline().DecorationDouble(); text.Span("."); }); }); } [Test] public void FontDecoration_Wavy() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a wavy-line text decoration").Underline().DecorationWavy(); text.Span("."); }); }); } [Test] public void FontDecoration_Dotted() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a dotted text decoration").Underline().DecorationDotted(); text.Span("."); }); }); } [Test] public void FontDecoration_Dashed() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a dashed text decoration").Underline().DecorationDashed(); text.Span("."); }); }); } #endregion #region Font Weight [Test] public void FontWeight_200_ExtraLight() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("an extra-light").ExtraLight().BackgroundColor(Colors.Grey.Lighten3); text.Span(" font weight."); }); }); } [Test] public void FontWeight_300_Light() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a light").Light().BackgroundColor(Colors.Grey.Lighten3); text.Span(" font weight."); }); }); } [Test] public void FontWeight_400_Regular() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a regular").NormalWeight().BackgroundColor(Colors.Grey.Lighten3); text.Span(" font weight."); }); }); } [Test] public void FontWeight_500_Medium() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a medium").Medium().BackgroundColor(Colors.Grey.Lighten3); text.Span(" font weight."); }); }); } [Test] public void FontWeight_600_SemiBold() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a semi-bold").SemiBold().BackgroundColor(Colors.Grey.Lighten3); text.Span(" font weight."); }); }); } [Test] public void FontWeight_700_Bold() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("a bold").Bold().BackgroundColor(Colors.Grey.Lighten3); text.Span(" font weight."); }); }); } [Test] public void FontWeight_800_ExtraBold() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("This example shows "); text.Span("an extra-bold").ExtraBold().BackgroundColor(Colors.Grey.Lighten3 ); text.Span(" font weight."); }); }); } #endregion #region Font Position [Test] public void FontPosition_Subscript() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Chemical formula of sulfuric acid: H"); text.Span("2").Subscript(); text.Span("SO"); text.Span("4").Subscript(); text.Span("."); }); }); } [Test] public void FontPosition_Superscript() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text(text => { text.Span("Enstein's equation: E=mc"); text.Span("2").Superscript(); }); }); } #endregion #region Font Features [Test] public void FontFeatures_StandardLigatures_Enabled() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text("final") .EnableFontFeature(FontFeatures.StandardLigatures); }); } [Test] public void FontFeatures_StandardLigatures_Disabled() { VisualTest.PerformWithDefaultPageSettings(container => { container .Text("final") .DisableFontFeature(FontFeatures.StandardLigatures); }); } #endregion } ================================================ FILE: Source/QuestPDF.VisualTests/VisualTestEngine.cs ================================================ using System.Globalization; using System.Text.RegularExpressions; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using SkiaSharp; namespace QuestPDF.VisualTests; public static class Helpers { public static TOutput Apply(this TInput input, Func func) { return func(input); } } public static class ImageComparer { public static bool AreImagesIdentical(SKBitmap bitmap1, SKBitmap bitmap2) { if (bitmap1.Width != bitmap2.Width || bitmap1.Height != bitmap2.Height) { Assert.Fail("Different image sizes: " + $"Image 1: {bitmap1.Width}x{bitmap1.Height}, " + $"Image 2: {bitmap2.Width}x{bitmap2.Height}"); } if (bitmap1.ColorType != bitmap2.ColorType) { Assert.Fail("Different image color types: " + $"Image 1: {bitmap1.ColorType}, " + $"Image 2: {bitmap2.ColorType}"); } var pixels1 = bitmap1.Pixels; var pixels2 = bitmap2.Pixels; if (pixels1.Length != pixels2.Length) { Assert.Fail("Different image pixel counts: " + $"Image 1: {pixels1.Length}, " + $"Image 2: {pixels2.Length}"); } var differences = pixels1.Zip(pixels2, (p1, p2) => new[] {p1.Red - p2.Red, p1.Green - p2.Green, p1.Blue - p2.Blue, p1.Alpha - p2.Alpha }) .Select(x => x.Select(Math.Abs)) .Select(x => x.Max()) .Where(diff => diff > 0) .ToArray(); if (differences.Length > 0) { var min = differences.Min(); var max = differences.Max(); var average = differences.Average(x => x); var message = $"Images differ by {min} (min), {max} (max), {average:F2} (avg). Different pixels: {differences.Length}."; Assert.Fail(message); } for (var i = 0; i < pixels1.Length; i++) { if (pixels1[i] != pixels2[i]) return false; } return true; } public static bool AreImagesIdentical(byte[] imageData1, byte[] imageData2) { using var bitmap1 = SKBitmap.Decode(imageData1); using var bitmap2 = SKBitmap.Decode(imageData2); return AreImagesIdentical(bitmap1, bitmap2); } } public static class VisualTestEngine { private static string ActualOutputDirectoryName => Path.Combine(TestContext.CurrentContext.TestDirectory, "ActualOutput"); private static string ExpectedOutputDirectoryName => Path.Combine(TestContext.CurrentContext.TestDirectory, "ExpectedOutput"); private static readonly Regex TestNameRegex = new(@"QuestPDF\.VisualTests\.(?.*)Tests"); public static void ClearActualOutputDirectories() { if (Directory.Exists(ActualOutputDirectoryName)) Directory.Delete(ActualOutputDirectoryName, true); } public static void ShouldMatchExpectedImage(this IDocument document) { if (TestContext.CurrentContext.Test.ClassName == null) throw new Exception("Test class name is not set."); var match = TestNameRegex.Match(TestContext.CurrentContext.Test.ClassName); var testCategory = match.Groups["name"].Value; var imageGenerationSettings = new ImageGenerationSettings { ImageFormat = ImageFormat.Png, RasterDpi = 144 }; var actualImages = document.GenerateImages(imageGenerationSettings).ToList(); var hasMultipleImages = actualImages.Count > 1; var actualOutputPath = Path.Combine(ActualOutputDirectoryName, testCategory); var expectedOutputPath = Path.Combine(ExpectedOutputDirectoryName, testCategory); var testName = TestContext.CurrentContext.Test.Name; Directory.CreateDirectory(actualOutputPath); string GetFileName(int index) { return hasMultipleImages ? $"{testName}_{index}.png" : $"{testName}.png"; } foreach (var i in Enumerable.Range(0, actualImages.Count)) { var actualImagePath = Path.Combine(actualOutputPath, GetFileName(i)); File.WriteAllBytes(actualImagePath, actualImages[i]); } if (!Directory.Exists(expectedOutputPath)) Assert.Inconclusive("Cannot find the expected output folder"); var expectedOutputFileCount = Directory.EnumerateFiles(expectedOutputPath, $"{testName}*.png").Count(); if (actualImages.Count != expectedOutputFileCount) Assert.Fail($"Generated {actualImages.Count} images but expected {expectedOutputFileCount}"); foreach (var i in Enumerable.Range(0, actualImages.Count)) { var expectedImagePath = Path.Combine(expectedOutputPath, GetFileName(i)); if (!File.Exists(expectedImagePath)) Assert.Fail($"Cannot find expected image file {expectedImagePath}"); var expectedImageBytes = File.ReadAllBytes(expectedImagePath); var actualImageBytes = actualImages[i]; var imagesAreIdentical = ImageComparer.AreImagesIdentical(actualImageBytes, expectedImageBytes); if (imagesAreIdentical) continue; var pageText = actualImages.Count > 1 ? $" (page {i})" : string.Empty; Assert.Fail($"Generated image does not match expected image{pageText}."); } } } public static class VisualTest { public static void Perform(Action documentBuilder) { SetUpCultureInfoToInvariant(); Document .Create(documentBuilder) .ShouldMatchExpectedImage(); } public static void PerformWithDefaultPageSettings(Action contentBuilder) { SetUpCultureInfoToInvariant(); Document .Create(document => { document.Page(page => { page.MinSize(new PageSize(0, 0)); page.MaxSize(new PageSize(1000, 1000)); page.Margin(20); page.PageColor(Colors.White); page.DefaultTextStyle(x => x.FontSize(16)); page.Content().Element(contentBuilder); }); }) .ShouldMatchExpectedImage(); } private static void SetUpCultureInfoToInvariant() { Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; } } ================================================ FILE: Source/QuestPDF.ZUGFeRD/GenerationTest.cs ================================================ using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.ZUGFeRD; public class Tests { [Test] public void ZUGFeRD_Test() { // TODO: Please make sure that you are eligible to use the Community license. // To learn more about the QuestPDF licensing, please visit: // https://www.questpdf.com/pricing.html QuestPDF.Settings.License = LicenseType.Community; Document .Create(document => { document.Page(page => { page.Content().Text("Your invoice content"); }); }) .WithMetadata(new DocumentMetadata { Title = "Conformance Test: ZUGFeRD", Author = "SampleCompany", Subject = "ZUGFeRD Test Document", Language = "en-US" }) .WithSettings(new DocumentSettings { PdfA = true }) // PDF/A-3b .GeneratePdf("invoice.pdf"); DocumentOperation .LoadFile("invoice.pdf") .AddAttachment(new DocumentOperation.DocumentAttachment { Key = "factur-zugferd", FilePath = "resource-factur-x.xml", AttachmentName = "factur-x.xml", MimeType = "text/xml", Description = "Factur-X Invoice", Relationship = DocumentOperation.DocumentAttachmentRelationship.Source, CreationDate = DateTime.UtcNow, ModificationDate = DateTime.UtcNow }) .ExtendMetadata(File.ReadAllText("resource-zugferd-metadata.xml")) .Save("zugferd-invoice.pdf"); } } ================================================ FILE: Source/QuestPDF.ZUGFeRD/QuestPDF.ZUGFeRD.csproj ================================================ net10.0 enable enable en false true all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive PreserveNewest PreserveNewest ================================================ FILE: Source/QuestPDF.ZUGFeRD/resource-factur-x.xml ================================================ urn:cen.eu:en16931:2017 RE-20201121/508 380 20201121 1 Design (hours) Of a sample invoice 160.0000 1.0000 160.0000 1.0000 1.0000 VAT S 7.00 160.00 2 Ballons various colors, ~2000ml 0.7900 1.0000 0.7900 1.0000 400.0000 VAT S 19.00 316.00 3 Hot air „heiße Luft“ (litres) 0.0250 1.0000 0.0250 1.0000 800.0000 VAT S 19.00 20.00 AB321 Bei Spiel GmbH 12345 Ecke 12 Stadthausen DE DE136695976 2 Theodor Est 88802 Bahnstr. 42 Spielkreis DE 20201110 RE-20201121/508 EUR 42 Bank transfer DE88200800000970375700 Max Mustermann COBADEFFXXX 11.20 VAT 160.00 S 7.00 63.84 VAT 336.00 S 19.00 Zahlbar ohne Abzug bis 12.12.2020 20201212 496.00 0.00 0.00 496.00 75.04 571.04 0.00 571.04 ================================================ FILE: Source/QuestPDF.ZUGFeRD/resource-zugferd-metadata.xml ================================================ EN 16931 INVOICE factur-x.xml 1.0 ZUGFeRD PDFA Extension Schema urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# fx DocumentFileName Text external name of the embedded XML invoice file DocumentType Text external INVOICE Version Text external The actual version of the ZUGFeRD XML schema ConformanceLevel Text external The selected ZUGFeRD profile completeness ================================================ FILE: Source/QuestPDF.slnx ================================================ ================================================ FILE: Source/global.json ================================================ { "sdk": { "version": "10.0.0", "rollForward": "latestMinor", "allowPrerelease": true } } ================================================ FILE: Source/nuget.config ================================================