Compare commits

..

70 Commits

Author SHA1 Message Date
MarcinZiabek 662a55e21c Implemented text shaping prototype 2022-03-01 23:22:14 +01:00
MarcinZiabek 7d825d123b Created text measuring algorithm for shaped text via HarfBuzz 2022-02-28 23:31:51 +01:00
MarcinZiabek 054e7a5f20 Added minimal API example 2022-02-28 15:41:10 +01:00
MarcinZiabek a92cc25fb1 Improved font matching and exception message. Refactored tests. 2022-02-27 15:19:50 +01:00
MarcinZiabek 8db283d559 Code refactorization + added support for multi typeface font types 2022-02-25 23:02:42 +01:00
Marcin Ziąbek 8a64abaac2 Merge pull request #106 from SvizelPritula/main
FontManager: Add support for multiple variants of font families registered at runtime
2022-02-21 22:26:51 +01:00
MarcinZiabek 68df390d0e Element renaming + custom text format 2022-02-21 00:57:53 +01:00
MarcinZiabek d73c694452 Added minimal API 2022-02-20 16:17:43 +01:00
MarcinZiabek 4479a49ad0 Improved support for page sections 2022-02-16 15:34:54 +01:00
MarcinZiabek 63288dfb60 Updated examples 2022-02-07 22:29:59 +01:00
MarcinZiabek fc325d8b76 2022.2.3 2022-02-07 22:25:48 +01:00
Marcin Ziąbek 5ffeaa3075 Merge pull request #119 from B-wareBS/features/unit-conversion-fix
Fixed an issue with unit conversion.
2022-02-07 22:04:55 +01:00
Chris de Jong-Pobedinskiy fe431fbc12 Fixed an issue with unit conversion.
Fixed an issue where unit conversions from mm to pt would be off by a
factor of 100.

This fixes #118
2022-02-07 13:56:20 +01:00
Marcin Ziąbek e0de408821 2022.2.2 Fixed regression: exception when Column/Row have no items 2022-02-01 23:39:43 +01:00
Marcin Ziąbek 8ce6afef5a Simplified readme 2022-02-01 11:14:38 +01:00
Marcin Ziąbek 1c70b20028 Added examples for execution order 2022-01-31 00:15:10 +01:00
Marcin Ziąbek 3aef4ec4c9 2022.2.1 2022-01-30 23:46:11 +01:00
Marcin Ziąbek 3649607bd9 Merge remote-tracking branch 'origin/2022.02'
# Conflicts:
#	QuestPDF.Examples/RowExamples.cs
#	QuestPDF.UnitTests/RowTests.cs
#	QuestPDF/Elements/Row.cs
#	QuestPDF/Fluent/RowExtensions.cs
#	QuestPDF/QuestPDF.csproj
#	QuestPDF/Resources/ReleaseNotes.txt
2022-01-30 23:44:20 +01:00
Marcin Ziąbek 9a6b11ab85 Updated examples and readme 2022-01-30 22:52:56 +01:00
Marcin Ziąbek ac29ed4794 2022.2.0-beta2 2022-01-26 22:26:47 +01:00
Marcin Ziąbek 5f7da8b5b3 Line: added default configuration 2022-01-26 22:26:34 +01:00
Benjamin Swart 78eef45ce2 FontManager: Add support for multiple variants of font families registered at runtime 2022-01-24 15:13:41 +01:00
Marcin Ziąbek 5424d9f0a0 2022.2.0-beta1 2022-01-24 14:53:01 +01:00
Marcin Ziąbek 946fe91a98 WriteOnlyStream refactorization 2022-01-24 00:34:27 +01:00
Marcin Ziąbek b5839d3989 Merge pull request #65 from schulz3000/main
fix rendering direct to aspnet core response stream
2022-01-24 00:17:14 +01:00
MarcinZiabek 2ca6d1cb47 Decoration: improved implementation, fixed unit tests 2022-01-23 23:08:18 +01:00
Marcin Ziąbek 49a3371af7 Added StopPaging element 2022-01-23 21:42:03 +01:00
Marcin Ziąbek 3f3ada6d8a Minor code adjustments 2022-01-23 21:41:45 +01:00
Marcin Ziąbek ba409a8712 Create codeql-analysis.yml 2022-01-21 03:11:22 +01:00
Marcin Ziąbek b96c67a8af Fixed Column tests 2022-01-21 00:09:39 +01:00
Marcin Ziąbek b9fbb9267f Obsolete methods refactorization 2022-01-18 23:24:10 +01:00
Marcin Ziąbek 272b0fc11a Added support for unit types 2022-01-18 23:14:57 +01:00
Marcin Ziąbek b2cc7bbd36 Renamed Decoration element 2022-01-16 17:08:57 +01:00
Marcin Ziąbek e3ce194fd1 Simple SVG example 2022-01-16 17:03:01 +01:00
Marcin Ziąbek 9b892c86ed 2022.1.0 2022-01-10 02:43:58 +01:00
Marcin Ziąbek c9833d410d 2022.1.0-beta6 2022-01-09 03:08:18 +01:00
Marcin Ziąbek f8057a840c Added: Line, Reimplemented: Column, Row and Decoration 2022-01-09 00:11:34 +01:00
Marcin Ziąbek a2ce31022c Improved default behavior of fluent configuration 2022-01-08 01:11:25 +01:00
Marcin Ziąbek 763fdfac0f Row: fixed paging behavior, Table: improved paging behavior for already rendered elements 2022-01-07 13:15:27 +01:00
Marcin Ziąbek ba8b35b1c0 Implemented ScaleToFit element 2022-01-07 00:09:25 +01:00
Marcin Ziąbek 648a359d15 Implemented the PreventPaging element 2022-01-06 22:19:33 +01:00
Marcin Ziąbek 630d3389d3 Table: added support for headers and footers 2022-01-06 21:36:27 +01:00
Marcin Ziąbek a7c3c88e81 Updated release notes 2022-01-06 19:57:22 +01:00
Marcin Ziąbek 9524cc6eb1 2022.1.0-beta3 Renaming Box() to MinimalBox() 2022-01-06 19:57:22 +01:00
Marcin Ziąbek 4152b67679 Table: row height fixes for overlapping cells. Added code examples 2022-01-06 19:57:22 +01:00
Marcin Ziąbek 8020cb30cb Table: improved repeating+paging support, added example 2022-01-06 19:57:22 +01:00
Marcin Ziąbek 9358fb8937 Fixed: traversing elements graph 2022-01-06 19:57:21 +01:00
Marcin Ziąbek eb297177ea VisitChildren -> GetChildren traversal refactorization 2022-01-06 19:57:21 +01:00
Marcin Ziąbek 7f651932b9 Table: added default style, fixed bugs, added examples 2022-01-06 19:57:21 +01:00
Marcin Ziąbek caaae5fa1c Table: fixed cross-cell size adjusting 2022-01-06 19:57:21 +01:00
Marcin Ziąbek f76caa28b1 Update Table.cs 2022-01-06 19:57:21 +01:00
MarcinZiabek ea92655ef5 Typo fixes 2022-01-06 19:57:21 +01:00
MarcinZiabek 3f8463e9b7 2022.1.0-beta0 2022-01-06 19:57:21 +01:00
MarcinZiabek 02e25b193d 2022.1.0-alpha0 2022-01-06 19:57:21 +01:00
MarcinZiabek 7cc272ff9e Border radius: added example 2022-01-06 19:57:21 +01:00
MarcinZiabek b8ea30be23 Improved readibility of the table layout algorithm 2022-01-06 19:57:21 +01:00
MarcinZiabek 06418238dc Table layout: refactorization 2022-01-06 19:57:21 +01:00
MarcinZiabek ed73c5b82d Table: layouting improvements for paging 2022-01-06 19:57:21 +01:00
MarcinZiabek 96732b5856 Fixed: corner cases of paging support 2022-01-06 19:57:21 +01:00
MarcinZiabek c7008ad940 Improved wrapping support 2022-01-06 19:57:21 +01:00
MarcinZiabek 940616cd3e Table: optimized rendering performance 2022-01-06 19:57:21 +01:00
MarcinZiabek 01a80032b0 Table: minor optimizations + performance test 2022-01-06 19:57:21 +01:00
MarcinZiabek 3e5e3843d2 Rendering algorithm partially working 2022-01-06 19:57:21 +01:00
MarcinZiabek d3911d5687 Table: refactorization, cell placement algorithm 2022-01-06 19:57:21 +01:00
Marcin Ziąbek 82e4dd36f0 Improved table paging support 2022-01-06 19:57:21 +01:00
Marcin Ziąbek 561d6a004c Row: added support for mixed columns (constant + relational), good when performing visual column merging 2022-01-06 19:57:20 +01:00
Marcin Ziąbek 59b77e55d4 Improved Row element stability 2022-01-06 19:57:20 +01:00
Marcin Ziąbek ea398c562f Merge pull request #64 from markhewett/us-page-sizes
Tweak US Letter and Legal page sizes
2022-01-04 19:31:57 +01:00
Xaver Schulz cb493562fc fix rendering direct to aspnet core response stream
adding a WrapperStream to support streams that not implement the Position property needed by SkiaSharp.
e.g. useful for the aspnet core response stream

fix #52
2021-12-10 21:07:42 +01:00
Hewett, Mark A 5b85ae2627 Tweak US Letter and Legal page sizes
Adobe Acrobat Reader DC now shows the correct sizes for letter and legal size documents.
2021-12-08 19:17:42 -06:00
115 changed files with 3025 additions and 1840 deletions
+70
View File
@@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '22 16 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'csharp' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+3 -3
View File
@@ -13,11 +13,11 @@ namespace QuestPDF.Examples
[Test]
public void Example()
{
FontManager.RegisterFontType("LibreBarcode39", File.OpenRead("LibreBarcode39-Regular.ttf"));
FontManager.RegisterFontType(File.OpenRead("LibreBarcode39-Regular.ttf"));
RenderingTest
.Create()
.PageSize(400, 100)
.PageSize(400, 200)
.ShowResults()
.Render(container =>
{
@@ -25,7 +25,7 @@ namespace QuestPDF.Examples
.Background(Colors.White)
.AlignCenter()
.AlignMiddle()
.Text("*QuestPDF*", TextStyle.Default.FontType("LibreBarcode39").Size(64));
.Text("*QuestPDF*", TextStyle.Default.FontType("Libre Barcode 39").Size(64));
});
}
}
+3 -3
View File
@@ -52,14 +52,14 @@ namespace QuestPDF.Examples
container
.Background(Colors.White)
.Padding(25)
.Stack(stack =>
.Column(column =>
{
stack
column
.Item()
.PaddingBottom(10)
.Text("Chart example", TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium));
stack
column
.Item()
.Border(1)
.ExtendHorizontal()
+56
View File
@@ -0,0 +1,56 @@
using System;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class ColumnExamples
{
[Test]
public void Column()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ShowResults()
.ProducePdf()
.Render(container =>
{
container.Column(column =>
{
foreach (var i in Enumerable.Range(0, 10))
column.Item().Element(Block);
static void Block(IContainer container)
{
container
.Width(72)
.Height(3.5f, Unit.Inch)
.Height(1.5f, Unit.Inch)
.Background(Placeholders.BackgroundColor());
}
});
});
}
[Test]
public void Stability_NoItems()
{
RenderingTest
.Create()
.ProducePdf()
.MaxPages(100)
.PageSize(250, 150)
.Render(container =>
{
container
.Padding(25)
.Column(column => { });
});
}
}
}
+5 -5
View File
@@ -36,8 +36,8 @@ namespace QuestPDF.Examples
.BorderColor(Colors.Black)
.Row(row =>
{
row.RelativeColumn().Element(x => GenerateStructure(x, level));
row.RelativeColumn().Element(x => GenerateStructure(x, level));
row.RelativeItem().Element(x => GenerateStructure(x, level));
row.RelativeItem().Element(x => GenerateStructure(x, level));
});
}
else
@@ -45,10 +45,10 @@ namespace QuestPDF.Examples
container
.Border(level / 4f)
.BorderColor(Colors.Black)
.Stack(stack =>
.Column(column =>
{
stack.Item().Element(x => GenerateStructure(x, level));
stack.Item().Element(x => GenerateStructure(x, level));
column.Item().Element(x => GenerateStructure(x, level));
column.Item().Element(x => GenerateStructure(x, level));
});
}
}
+2 -2
View File
@@ -22,10 +22,10 @@ namespace QuestPDF.Examples
page.Header().Text("Header");
page.Content().PaddingVertical(10).Border(1).Padding(10).Stack(stack =>
page.Content().PaddingVertical(10).Border(1).Padding(10).Column(column =>
{
foreach (var index in Enumerable.Range(1, 100))
stack.Item().Text($"Line {index}", TextStyle.Default.Color(Placeholders.Color()));
column.Item().Text($"Line {index}", TextStyle.Default.Color(Placeholders.Color()));
});
page.Footer().Text("Footer");
+2 -2
View File
@@ -9,7 +9,7 @@ namespace QuestPDF.Examples
public class DebuggingTesting
{
[Test]
public void Stack()
public void Column()
{
Assert.Throws<DocumentLayoutException>(() =>
{
@@ -23,7 +23,7 @@ namespace QuestPDF.Examples
.Width(100)
.Background(Colors.Grey.Lighten3)
.DebugPointer("Example debug pointer")
.Stack(x =>
.Column(x =>
{
x.Item().Text("Test");
x.Item().Width(150);
+4 -4
View File
@@ -13,7 +13,7 @@ namespace QuestPDF.Examples
{
RenderingTest
.Create()
.ProduceImages()
.ProducePdf()
.ShowResults()
.RenderDocument(container =>
{
@@ -26,11 +26,11 @@ namespace QuestPDF.Examples
page.Size(PageSizes.A4);
page.Background(Colors.White);
page.Content().Stack(stack =>
page.Content().Column(column =>
{
stack.Item().Text(Placeholders.Sentence());
column.Item().Text(Placeholders.Sentence());
stack.Item().Text(text =>
column.Item().Text(text =>
{
// text in this block is additionally semibold
text.DefaultTextStyle(TextStyle.Default.SemiBold());
@@ -24,12 +24,12 @@ namespace QuestPDF.Examples
container
.Padding(10)
.DefaultTextStyle(TextStyle.Default.Bold().Underline())
.Stack(stack =>
.Column(column =>
{
stack.Item().Text("Default style applies to all children", TextStyle.Default);
stack.Item().Text("You can override certain styles", TextStyle.Default.Underline(false).Color(Colors.Green.Darken2));
column.Item().Text("Default style applies to all children", TextStyle.Default);
column.Item().Text("You can override certain styles", TextStyle.Default.Underline(false).Color(Colors.Green.Darken2));
stack.Item().PaddingTop(10).Border(1).Grid(grid =>
column.Item().PaddingTop(10).Border(1).Grid(grid =>
{
grid.Columns(4);
@@ -23,21 +23,21 @@ namespace QuestPDF.Examples
container.Page(page =>
{
page.Size(PageSizes.A6);
page.Margin(30);
page.Margin(5);
page.Background(Colors.White);
page.Header().Stack(stack =>
page.Header().Column(column =>
{
stack.Item().ShowOnce().Background(Colors.Blue.Lighten2).Height(60);
stack.Item().SkipOnce().Background(Colors.Green.Lighten2).Height(40);
column.Item().ShowOnce().Background(Colors.Blue.Lighten2).Height(60);
column.Item().SkipOnce().Background(Colors.Green.Lighten2).Height(40);
});
page.Content().PaddingVertical(10).Stack(stack =>
page.Content().PaddingVertical(10).Column(column =>
{
stack.Spacing(10);
column.Spacing(10);
foreach (var _ in Enumerable.Range(0, 13))
stack.Item().Background(Colors.Grey.Lighten2).Height(40);
column.Item().Background(Colors.Grey.Lighten2).Height(40);
});
page.Footer().AlignCenter().Text(text =>
+46 -46
View File
@@ -40,7 +40,7 @@ namespace QuestPDF.Examples
.Decoration(decoration =>
{
decoration
.Header()
.Before()
.Background(Colors.Grey.Medium)
.Padding(10)
.Text("Notes", TextStyle.Default.Size(16).Color("#FFF"));
@@ -66,27 +66,27 @@ namespace QuestPDF.Examples
container
.Background("#FFF")
.Padding(20)
.Stack(stack =>
.Column(column =>
{
stack.Item()
column.Item()
.PaddingBottom(10)
.AlignCenter()
.Text("This Row element is 700pt wide");
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.ConstantColumn(100)
row.ConstantItem(100)
.Background(Colors.Grey.Lighten1)
.Padding(10)
.ExtendVertical()
.Text("This column is 100 pt wide");
row.RelativeColumn()
row.RelativeItem()
.Background(Colors.Grey.Lighten2)
.Padding(10)
.Text("This column takes 1/3 of the available space (200pt)");
row.RelativeColumn(2)
row.RelativeItem(2)
.Background(Colors.Grey.Lighten3)
.Padding(10)
.Text("This column takes 2/3 of the available space (400pt)");
@@ -109,15 +109,15 @@ namespace QuestPDF.Examples
.Row(row =>
{
row.Spacing(20);
row.RelativeColumn(2).Border(1).Background(Colors.Grey.Lighten1);
row.RelativeColumn(3).Border(1).Background(Colors.Grey.Lighten2);
row.RelativeColumn(4).Border(1).Background(Colors.Grey.Lighten3);
row.RelativeItem(2).Border(1).Background(Colors.Grey.Lighten1);
row.RelativeItem(3).Border(1).Background(Colors.Grey.Lighten2);
row.RelativeItem(4).Border(1).Background(Colors.Grey.Lighten3);
});
});
}
[Test]
public void Stack()
public void column()
{
RenderingTest
.Create()
@@ -127,13 +127,13 @@ namespace QuestPDF.Examples
container
.Background("#FFF")
.Padding(15)
.Stack(stack =>
.Column(column =>
{
stack.Spacing(15);
column.Spacing(15);
stack.Item().Background(Colors.Grey.Medium).Height(50);
stack.Item().Background(Colors.Grey.Lighten1).Height(100);
stack.Item().Background(Colors.Grey.Lighten2).Height(150);
column.Item().Background(Colors.Grey.Medium).Height(50);
column.Item().Background(Colors.Grey.Lighten1).Height(100);
column.Item().Background(Colors.Grey.Lighten2).Height(150);
});
});
}
@@ -267,12 +267,12 @@ namespace QuestPDF.Examples
layers
.PrimaryLayer()
.Padding(25)
.Stack(stack =>
.Column(column =>
{
stack.Spacing(5);
column.Spacing(5);
foreach (var _ in Enumerable.Range(0, 7))
stack.Item().Text(Placeholders.Sentence());
column.Item().Text(Placeholders.Sentence());
});
// layer above the main content
@@ -304,16 +304,16 @@ namespace QuestPDF.Examples
// {
// page.Header().PageNumber("Page {pdf:currentPage}");
//
// page.Content().Height(300).Stack(content =>
// page.Content().Height(300).column(content =>
// {
// content.Item().Height(200).Background(Colors.Grey.Lighten2);
//
// content.Item().EnsureSpace(100).Stack(stack =>
// content.Item().EnsureSpace(100).column(column =>
// {
// stack.Spacing(10);
// column.Spacing(10);
//
// foreach (var _ in Enumerable.Range(0, 4))
// stack.Item().Height(50).Background(Colors.Green.Lighten1);
// column.Item().Height(50).Background(Colors.Green.Lighten1);
// });
// });
// });
@@ -378,7 +378,7 @@ namespace QuestPDF.Examples
.Row(row =>
{
foreach (var color in colors)
row.RelativeColumn().Background(color);
row.RelativeItem().Background(color);
});
});
}
@@ -437,10 +437,10 @@ namespace QuestPDF.Examples
{
layers.Layer().Text("Something else");
layers.PrimaryLayer().Stack(stack =>
layers.PrimaryLayer().Column(column =>
{
stack.Item().PaddingTop(20).Text("Text 1");
stack.Item().PaddingTop(40).Text("Text 2");
column.Item().PaddingTop(20).Text("Text 1");
column.Item().PaddingTop(40).Text("Text 2");
});
layers.Layer().Canvas((canvas, size) =>
@@ -501,13 +501,13 @@ namespace QuestPDF.Examples
.SemiBold();
decoration
.Header()
.Before()
.PaddingBottom(10)
.Text("Example: scale component", headerFontStyle);
decoration
.Content()
.Stack(stack =>
.Column(column =>
{
var scales = new[] { 0.8f, 0.9f, 1.1f, 1.2f };
@@ -519,7 +519,7 @@ namespace QuestPDF.Examples
var fontStyle = TextStyle.Default.Size(16);
stack
column
.Item()
.Border(1)
.Background(fontColor)
@@ -708,14 +708,14 @@ namespace QuestPDF.Examples
.Border(2)
.Row(row =>
{
row.ConstantColumn(25)
row.ConstantItem(25)
.Border(1)
.RotateLeft()
.AlignCenter()
.AlignMiddle()
.Text("Sample text");
row.RelativeColumn().Border(1).Padding(5).Text(Placeholders.Paragraph());
row.RelativeItem().Border(1).Padding(5).Text(Placeholders.Paragraph());
});
});
}
@@ -731,11 +731,11 @@ namespace QuestPDF.Examples
container
.Padding(25)
.PaddingLeft(75)
.Stack(stack =>
.Column(column =>
{
stack.Item().Width(300).Height(150).Background(Colors.Blue.Lighten4);
column.Item().Width(300).Height(150).Background(Colors.Blue.Lighten4);
stack
column
.Item()
// creates an infinite space for its child
@@ -751,7 +751,7 @@ namespace QuestPDF.Examples
.Background(Colors.Blue.Darken1);
stack.Item().Width(300).Height(150).Background(Colors.Blue.Lighten3);
column.Item().Width(300).Height(150).Background(Colors.Blue.Lighten3);
});
});
}
@@ -766,13 +766,13 @@ namespace QuestPDF.Examples
{
container
.Padding(25)
.Stack(stack =>
.Column(column =>
{
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn().LabelCell("Label 1");
row.RelativeItem().LabelCell("Label 1");
row.RelativeColumn(3).Grid(grid =>
row.RelativeItem(3).Grid(grid =>
{
grid.Columns(3);
@@ -784,11 +784,11 @@ namespace QuestPDF.Examples
});
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn().ValueCell().Text("Value 1");
row.RelativeItem().ValueCell().Text("Value 1");
row.RelativeColumn(3).Grid(grid =>
row.RelativeItem(3).Grid(grid =>
{
grid.Columns(3);
@@ -800,10 +800,10 @@ namespace QuestPDF.Examples
});
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn().LabelCell("Label 6");
row.RelativeColumn().ValueCell().Text("Value 6");
row.RelativeItem().LabelCell("Label 6");
row.RelativeItem().ValueCell().Text("Value 6");
});
});
});
+3 -3
View File
@@ -25,15 +25,15 @@ namespace QuestPDF.Examples
page.Header().Text("With ensure space", TextStyle.Default.SemiBold());
page.Content().Stack(stack =>
page.Content().Column(column =>
{
stack
column
.Item()
.ExtendHorizontal()
.Height(75)
.Background(Colors.Grey.Lighten2);
stack
column
.Item()
.EnsureSpace(100)
.Text(Placeholders.LoremIpsum());
@@ -0,0 +1,76 @@
using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class ExecutionOrderExamples
{
[Test]
public void Example()
{
RenderingTest
.Create()
.PageSize(400, 300)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.DefaultTextStyle(TextStyle.Default.Size(18))
.Padding(25)
.Row(row =>
{
row.Spacing(25);
row.RelativeItem()
.Border(1)
.Padding(15)
.Background(Colors.Grey.Lighten2)
.Text("Lorem ipsum");
row.RelativeItem()
.Border(1)
.Background(Colors.Grey.Lighten2)
.Padding(15)
.Text("dolor sit amet");
});
});
}
[Test]
public void Example2()
{
RenderingTest
.Create()
.PageSize(200, 200)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.Border(2)
.Width(150)
.Height(150)
.Background(Colors.Blue.Lighten2)
.PaddingTop(50)
.Background(Colors.Green.Lighten2)
.PaddingRight(50)
.Background(Colors.Red.Lighten2)
.PaddingBottom(50)
.Background(Colors.Amber.Lighten2)
.PaddingLeft(50)
.Background(Colors.Grey.Lighten2);
});
}
}
}
+4 -4
View File
@@ -36,14 +36,14 @@ namespace QuestPDF.Examples
container
.Background("#FFF")
.Padding(25)
.Stack(stack =>
.Column(column =>
{
for(var i = 1; i <= 4; i++)
{
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(2).LabelCell(Placeholders.Label());
row.RelativeColumn(3).ValueCell().Text(Placeholders.Paragraph());
row.RelativeItem(2).LabelCell(Placeholders.Label());
row.RelativeItem(3).ValueCell().Text(Placeholders.Paragraph());
});
}
});
+5 -5
View File
@@ -19,17 +19,17 @@ namespace QuestPDF.Examples
.ShowResults()
.Render(page =>
{
page.Padding(25).Stack(stack =>
page.Padding(25).Column(column =>
{
stack.Spacing(25);
column.Spacing(25);
stack.Item().Image("logo.png");
column.Item().Image("logo.png");
var binaryData = File.ReadAllBytes("logo.png");
stack.Item().Image(binaryData);
column.Item().Image(binaryData);
using var stream = new FileStream("logo.png", FileMode.Open);
stack.Item().Image(stream);
column.Item().Image(stream);
});
});
}
+1 -1
View File
@@ -25,7 +25,7 @@ namespace QuestPDF.Examples
.Padding(25)
.Decoration(decoration =>
{
decoration.Header().Text(text =>
decoration.Before().Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.Size(20));
+57
View File
@@ -0,0 +1,57 @@
using System.IO;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class LineExamples
{
[Test]
public void LineHorizontal()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(15)
.MinimalBox()
.DefaultTextStyle(TextStyle.Default.Size(16))
.Column(column =>
{
column.Item().Text("Above text");
column.Item().PaddingVertical(5).LineHorizontal(1).LineColor(Colors.Grey.Medium);
column.Item().Text("Below text");
});
});
}
[Test]
public void LineVertical()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(15)
.DefaultTextStyle(TextStyle.Default.Size(16))
.Row(row =>
{
row.AutoItem().Text("Left text");
row.AutoItem().PaddingHorizontal(10).LineVertical(1).LineColor(Colors.Grey.Medium);
row.AutoItem().Text("Right text");
});
});
}
}
}
+1 -1
View File
@@ -43,7 +43,7 @@ namespace QuestPDF.Examples
container
.Background("#FFF")
.Padding(25)
.Stack(column =>
.Column(column =>
{
column.Spacing(10);
+44
View File
@@ -0,0 +1,44 @@
using System.Diagnostics;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using ShimSkiaSharp;
using SKTypeface = SkiaSharp.SKTypeface;
namespace QuestPDF.Examples
{
public class MinimalApiExamples
{
[Test]
public void MinimalApi()
{
Document
.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A5);
page.Margin(1.5f, Unit.Centimetre);
page.Header()
.Text("Hello PDF!", TextStyle.Default.SemiBold().Size(20));
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(10);
x.Item().Text(Placeholders.LoremIpsum());
x.Item().Image(Placeholders.Image(200, 100));
});
});
})
.GeneratePdf("hello.pdf");
Process.Start("explorer.exe", "hello.pdf");
}
}
}
+6 -6
View File
@@ -63,7 +63,7 @@ namespace QuestPDF.Examples
.Render(container =>
{
container
.Stack(column =>
.Column(column =>
{
column
.Item()
@@ -113,14 +113,14 @@ namespace QuestPDF.Examples
.Render(container =>
{
container
.Stack(column =>
.Column(column =>
{
column
.Item()
.Height(150)
.Row(row =>
{
row.RelativeColumn()
row.RelativeItem()
.Extend()
.Background("FFF")
@@ -128,7 +128,7 @@ namespace QuestPDF.Examples
.Width(50)
.Background("444");
row.RelativeColumn()
row.RelativeItem()
.Extend()
.Background("BBB")
@@ -142,7 +142,7 @@ namespace QuestPDF.Examples
.Height(150)
.Row(row =>
{
row.RelativeColumn()
row.RelativeItem()
.Extend()
.Background("BBB")
@@ -150,7 +150,7 @@ namespace QuestPDF.Examples
.Width(50)
.Background("444");
row.RelativeColumn()
row.RelativeItem()
.Extend()
.Background("BBB")
@@ -12,6 +12,7 @@
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="Svg.Skia" Version="0.5.10" />
</ItemGroup>
<ItemGroup>
@@ -28,6 +29,9 @@
<None Update="logo.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="pdf-icon.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+36 -21
View File
@@ -9,7 +9,7 @@ namespace QuestPDF.Examples
public class RowExamples
{
[Test]
public void ColumnTypes()
public void ItemTypes()
{
RenderingTest
.Create()
@@ -22,25 +22,24 @@ namespace QuestPDF.Examples
.Padding(25)
.MinimalBox()
.Border(1)
.Stack(stack =>
.Column(column =>
{
stack.Item().LabelCell("Total width: 600px");
column.Item().LabelCell("Total width: 600px");
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.ConstantColumn(150).ValueCell("150px");
row.ConstantColumn(100).ValueCell("100px");
row.RelativeColumn(4).ValueCell("200px");
row.RelativeColumn(3).ValueCell("150px");
row.ConstantItem(150).ValueCell("150px");
row.ConstantItem(100).ValueCell("100px");
row.RelativeItem(4).ValueCell("200px");
row.RelativeItem(3).ValueCell("150px");
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.ConstantColumn(100).ValueCell("100px");
row.ConstantColumn(50).ValueCell("50px");
row.Column(constantWidth: 100, relativeWidth: 4).ValueCell("350px");
row.RelativeColumn(2).ValueCell("100px");
row.RelativeColumn(1).ValueCell("50px");
row.ConstantItem(100).ValueCell("100px");
row.ConstantItem(50).ValueCell("50px");
row.RelativeItem(2).ValueCell("100px");
row.RelativeItem(1).ValueCell("50px");
});
});
});
@@ -63,17 +62,17 @@ namespace QuestPDF.Examples
.Padding(25)
.Row(row =>
{
row.RelativeColumn().Stack(stack =>
row.RelativeItem().Column(column =>
{
stack.Item().ShowOnce().Element(CreateBox).Text("X");
stack.Item().Element(CreateBox).Text("1");
stack.Item().Element(CreateBox).Text("2");
column.Item().ShowOnce().Element(CreateBox).Text("X");
column.Item().Element(CreateBox).Text("1");
column.Item().Element(CreateBox).Text("2");
});
row.RelativeColumn().Stack(stack =>
row.RelativeItem().Column(column =>
{
stack.Item().Element(CreateBox).Text("1");
stack.Item().Element(CreateBox).Text("2");
column.Item().Element(CreateBox).Text("1");
column.Item().Element(CreateBox).Text("2");
});
});
});
@@ -90,5 +89,21 @@ namespace QuestPDF.Examples
.ShowOnce();
}
}
[Test]
public void Stability_NoItems()
{
RenderingTest
.Create()
.ProducePdf()
.MaxPages(100)
.PageSize(250, 150)
.Render(container =>
{
container
.Padding(25)
.Row(row => { });
});
}
}
}
+42
View File
@@ -0,0 +1,42 @@
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class ScaleToFitExamples
{
[Test]
public void ScaleToFit()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container.Padding(25).Column(column =>
{
var text = Placeholders.Paragraph();
foreach (var i in Enumerable.Range(2, 5))
{
column
.Item()
.MinimalBox()
.Border(1)
.Padding(5)
.Width(i * 40)
.Height(i * 20)
.ScaleToFit()
.Text(text);
}
});
});
}
}
}
+2 -2
View File
@@ -27,14 +27,14 @@ namespace QuestPDF.Examples
page.Content().PaddingVertical(5).Row(row =>
{
row.RelativeColumn()
row.RelativeItem()
.Background(Colors.Grey.Lighten2)
.Border(1)
.Padding(5)
.ShowOnce()
.Text(Placeholders.Label());
row.RelativeColumn(2)
row.RelativeItem(2)
.Border(1)
.Padding(5)
.Text(Placeholders.Paragraph());
+3 -3
View File
@@ -23,10 +23,10 @@ namespace QuestPDF.Examples
page.Size(PageSizes.A7.Landscape());
page.Background(Colors.White);
page.Header().Stack(stack =>
page.Header().Column(column =>
{
stack.Item().ShowOnce().Text("This header is visible on the first page.");
stack.Item().SkipOnce().Text("This header is visible on the second page and all following.");
column.Item().ShowOnce().Text("This header is visible on the first page.");
column.Item().SkipOnce().Text("This header is visible on the second page and all following.");
});
page.Content()
+48
View File
@@ -0,0 +1,48 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class StopPaging
{
[Test]
public void Example()
{
RenderingTest
.Create()
.PageSize(300, 250)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.DefaultTextStyle(TextStyle.Default.Size(14))
.Decoration(decoration =>
{
decoration
.Before()
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.SemiBold().Color(Colors.Blue.Medium));
text.Span("Page ");
text.CurrentPageNumber();
});
decoration
.Content()
.Column(column =>
{
column.Spacing(25);
column.Item().StopPaging().Text(Placeholders.LoremIpsum());
column.Item().ExtendHorizontal().Height(75).Background(Colors.Grey.Lighten2);
});
});
});
}
}
}
+36
View File
@@ -0,0 +1,36 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
using Svg.Skia;
namespace QuestPDF.Examples
{
public class SvgImageExample
{
[Test]
public void BorderRadius()
{
RenderingTest
.Create()
.PageSize(175, 100)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Background(Colors.Grey.Lighten2)
.Padding(25)
.Canvas((canvas, space) =>
{
using var svg = new SKSvg();
svg.Load("pdf-icon.svg");
canvas.DrawPicture(svg.Picture);
});
});
}
}
}
+37 -2
View File
@@ -49,6 +49,39 @@ namespace QuestPDF.Examples
});
}
[Test]
public void PagingSupport()
{
RenderingTest
.Create()
.ProducePdf()
.PageSize(420, 220)
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
// by using custom 'Element' method, we can reuse visual configuration
table.Cell().Element(Block).Text(Placeholders.Label());
table.Cell().Element(Block).Text(Placeholders.Label());
table.Cell().Element(Block).Text(Placeholders.Paragraph());
table.Cell().Element(Block).Text(Placeholders.Label());
});
});
}
[Test]
public void DefaultCellStyle()
{
@@ -217,7 +250,7 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.ProduceImages()
.PageSize(220, 220)
.PageSize(220, 170)
.ShowResults()
.Render(container =>
{
@@ -380,7 +413,7 @@ namespace QuestPDF.Examples
.EnableCaching()
.EnableDebugging(false)
.ShowResults()
.Render(container => GeneratePerformanceStructure(container, 1000));
.Render(container => GeneratePerformanceStructure(container, 250));
}
public static void GeneratePerformanceStructure(IContainer container, int repeats)
@@ -456,8 +489,10 @@ namespace QuestPDF.Examples
return container
.Border(1)
.Background(Colors.Grey.Lighten3)
.ShowOnce()
.MinWidth(50)
.MinHeight(50)
.Padding(10)
.AlignCenter()
.AlignMiddle();
}
+28 -28
View File
@@ -117,19 +117,19 @@ namespace QuestPDF.Examples
{
decoration
.Content()
.Stack(stack =>
.Column(column =>
{
stack.Item().Element(Title);
stack.Item().PageBreak();
stack.Item().Element(TableOfContents);
stack.Item().PageBreak();
column.Item().Element(Title);
column.Item().PageBreak();
column.Item().Element(TableOfContents);
column.Item().PageBreak();
Chapters(stack);
Chapters(column);
stack.Item().Element(Acknowledgements);
column.Item().Element(Acknowledgements);
});
decoration.Footer().Element(Footer);
decoration.After().Element(Footer);
});
}
@@ -139,61 +139,61 @@ namespace QuestPDF.Examples
.Extend()
.PaddingBottom(200)
.AlignBottom()
.Stack(stack =>
.Column(column =>
{
stack.Item().Text("Quo Vadis", TextStyle.Default.Size(72).Bold().Color(Colors.Blue.Darken2));
stack.Item().Text("Henryk Sienkiewicz", TextStyle.Default.Size(24).Color(Colors.Grey.Darken2));
column.Item().Text("Quo Vadis", TextStyle.Default.Size(72).Bold().Color(Colors.Blue.Darken2));
column.Item().Text("Henryk Sienkiewicz", TextStyle.Default.Size(24).Color(Colors.Grey.Darken2));
});
}
void TableOfContents(IContainer container)
{
container.Stack(stack =>
container.Column(column =>
{
SectionTitle(stack, "Spis treści");
SectionTitle(column, "Spis treści");
foreach (var chapter in chapters)
{
stack.Item().InternalLink(chapter.Title).Row(row =>
column.Item().InternalLink(chapter.Title).Row(row =>
{
row.RelativeColumn().Text(chapter.Title, normalStyle);
row.ConstantColumn(100).AlignRight().Text(text => text.PageNumberOfLocation(chapter.Title, normalStyle));
row.RelativeItem().Text(chapter.Title, normalStyle);
row.ConstantItem(100).AlignRight().Text(text => text.PageNumberOfLocation(chapter.Title, normalStyle));
});
}
});
}
void Chapters(StackDescriptor stack)
void Chapters(ColumnDescriptor column)
{
foreach (var chapter in chapters)
{
stack.Item().Element(container => Chapter(container, chapter.Title, chapter.Content));
column.Item().Element(container => Chapter(container, chapter.Title, chapter.Content));
}
}
void Chapter(IContainer container, string title, string content)
{
container.Stack(stack =>
container.Column(column =>
{
SectionTitle(stack, title);
SectionTitle(column, title);
stack.Item().Text(text =>
column.Item().Text(text =>
{
text.ParagraphSpacing(5);
text.Span(content, normalStyle);
});
stack.Item().PageBreak();
column.Item().PageBreak();
});
}
void Acknowledgements(IContainer container)
{
container.Stack(stack =>
container.Column(column =>
{
SectionTitle(stack, "Podziękowania");
SectionTitle(column, "Podziękowania");
stack.Item().Text(text =>
column.Item().Text(text =>
{
text.DefaultTextStyle(normalStyle);
@@ -204,10 +204,10 @@ namespace QuestPDF.Examples
});
}
void SectionTitle(StackDescriptor stack, string text)
void SectionTitle(ColumnDescriptor column, string text)
{
stack.Item().Location(text).Text(text, subtitleStyle);
stack.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal();
column.Item().Location(text).Text(text, subtitleStyle);
column.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal();
}
void Footer(IContainer container)
+3 -3
View File
@@ -126,7 +126,7 @@ namespace QuestPDF.Examples
}
[Test]
public void TextStack()
public void Textcolumn()
{
RenderingTest
.Create()
@@ -236,7 +236,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default);
text.DefaultTextStyle(TextStyle.Default.BackgroundColor(Colors.Grey.Lighten3));
text.AlignLeft();
text.ParagraphSpacing(10);
@@ -251,7 +251,7 @@ namespace QuestPDF.Examples
text.EmptyLine();
foreach (var i in Enumerable.Range(1, 100))
foreach (var i in Enumerable.Range(1, 1000))
{
text.Line($"{i}: {Placeholders.Paragraph()}");
+161
View File
@@ -0,0 +1,161 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Examples
{
public class TextShapingTests
{
[Test]
public void ShapeText()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
using var backgroundPaint = new SKPaint
{
Color = SKColors.LightGray
};
RenderingTest
.Create()
.PageSize(550, 250)
.ProduceImages()
.ShowResults()
.Render(container =>
{
//var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus.";
//var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور";
var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
var text = arabic;
var metrics = textPaint.FontMetrics;
container
.Padding(25)
.Canvas((canvas, space) =>
{
canvas.Translate(0, 20);
var width = MeasureText(text, textPaint);
var widthReal = textPaint.MeasureText(text);
canvas.DrawRect(0, metrics.Descent, width, metrics.Ascent - metrics.Descent, backgroundPaint);
canvas.DrawShapedText(text, 0, 0, textPaint);
canvas.Translate(0, 40);
canvas.DrawText(text, 0, 0, textPaint);
});
});
}
float MeasureText(string text, SKPaint paint)
{
var font = paint.ToFont();
using var shaper = new SKShaper(paint.Typeface);
var result = shaper.Shape(text + " ", paint);
return Math.Max(result.Points.First().X, result.Points.Last().X);
var glyphCount = result.Codepoints.Length;
var glyphIds = new ushort[glyphCount];
var glyphWidths = new float[glyphCount];
var glyphBounds = new SKRect[glyphCount];
font.GetGlyphs(result.Codepoints.Select(x => (int)x).ToArray(), glyphIds);
font.GetGlyphWidths(glyphIds, glyphWidths, glyphBounds);
return glyphWidths.Sum();
return result.Points.Max(p => p.X);
using var skTextBlobBuilder = new SKTextBlobBuilder();
var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(font, result.Codepoints.Length);
var glyphSpan = positionedRunBuffer.GetGlyphSpan();
var positionSpan = positionedRunBuffer.GetPositionSpan();
for (int index = 0; index < result.Codepoints.Length; ++index)
{
glyphSpan[index] = (ushort) result.Codepoints[index];
positionSpan[index] = result.Points[index];
}
using var text1 = skTextBlobBuilder.Build();
return text1.Bounds.Width;
}
[Test]
public void ShapeSingle()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
var text = string.Join(" ", Enumerable.Range(0, 10_000).Select(x => Placeholders.Sentence()));
using var shaper = new SKShaper(textPaint.Typeface);
var result = shaper.Shape(text + " ", textPaint);
}
[Test]
public void ShapeMany()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
var text = string.Join(" ", Enumerable.Range(0, 1_000).Select(x => Placeholders.Sentence()));
foreach (var part in Regex.Split(text, "( )|(\\w+)"))
{
using var shaper = new SKShaper(textPaint.Typeface);
var result = shaper.Shape(part + " ", textPaint);
}
}
[Test]
public void ShapeMany2()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
using var shaper = new SKShaper(textPaint.Typeface);
foreach (var i in Enumerable.Range(0, 10_000))
{
var text = Placeholders.Paragraph();
textPaint.MeasureText(text);
var result = shaper.Shape(text + " ", textPaint);
}
}
}
}
+149
View File
@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="75.320129mm"
height="92.604164mm"
viewBox="0 0 75.320129 92.604164"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="PDF file icon.svg">
<title
id="title2682">PDF file icon</title>
<defs
id="defs2">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath839">
<rect
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"
id="rect841"
width="68.688263"
height="67.886459"
x="-74.033661"
y="141.44913" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="122.12727"
inkscape:cy="167.02823"
inkscape:document-units="mm"
inkscape:current-layer="g899"
showgrid="false"
inkscape:object-nodes="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-page="false"
inkscape:snap-others="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>PDF file icon</dc:title>
<dc:date>08/10/2018</dc:date>
<dc:creator>
<cc:Agent>
<dc:title>Adobe Systems</dc:title>
</cc:Agent>
</dc:creator>
<dc:publisher>
<cc:Agent>
<dc:title>CMetalCore</dc:title>
</cc:Agent>
</dc:publisher>
<dc:description>Fuente del texto &quot;PDF&quot;:
Franklin Gothic Medium Cond</dc:description>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(36.076172,-93.731774)">
<g
id="g899"
transform="matrix(1.4843054,0,0,1.4843054,17.471885,-90.243502)">
<g
id="g876">
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ff2116;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -29.632812,123.94727 c -3.551967,0 -6.44336,2.89347 -6.44336,6.44531 v 49.49804 c 0,3.55185 2.891393,6.44532 6.44336,6.44532 H 8.2167969 c 3.5519661,0 6.4433591,-2.89335 6.4433591,-6.44532 v -40.70117 c 0,0 0.101353,-1.19181 -0.416015,-2.35156 -0.484969,-1.08711 -1.275391,-1.84375 -1.275391,-1.84375 a 1.0584391,1.0584391 0 0 0 -0.0059,-0.008 L 3.5722246,125.7752 a 1.0584391,1.0584391 0 0 0 -0.015625,-0.0156 c 0,0 -0.8017392,-0.76344 -1.9902344,-1.27344 -1.39939552,-0.6005 -2.8417968,-0.53711 -2.8417968,-0.53711 l 0.021484,-0.002 z"
id="path890" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -29.632812,126.06445 h 28.3789058 a 1.0584391,1.0584391 0 0 0 0.021484,0 c 0,0 1.13480448,0.011 1.96484378,0.36719 0.79889772,0.34282 1.36536982,0.86176 1.36914062,0.86524 1.25e-5,1e-5 0.00391,0.004 0.00391,0.004 l 9.3671868,9.18945 c 0,0 0.564354,0.59582 0.837891,1.20899 0.220779,0.49491 0.234375,1.40039 0.234375,1.40039 a 1.0584391,1.0584391 0 0 0 -0.002,0.0449 v 40.74609 c 0,2.41592 -1.910258,4.32813 -4.3261717,4.32813 H -29.632812 c -2.415914,0 -4.326172,-1.91209 -4.326172,-4.32813 v -49.49804 c 0,-2.41603 1.910258,-4.32813 4.326172,-4.32813 z"
id="rect2684" />
<path
sodipodi:nodetypes="scccscccscccccssscacccsccccccccccccccccccccaccccccccc"
inkscape:connector-curvature="0"
d="m -23.40766,161.09299 c -1.45669,-1.45669 0.11934,-3.45839 4.39648,-5.58397 l 2.69124,-1.33743 1.04845,-2.29399 c 0.57665,-1.26169 1.43729,-3.32036 1.91254,-4.5748 l 0.8641,-2.28082 -0.59546,-1.68793 c -0.73217,-2.07547 -0.99326,-5.19438 -0.52872,-6.31588 0.62923,-1.51909 2.69029,-1.36323 3.50626,0.26515 0.63727,1.27176 0.57212,3.57488 -0.18329,6.47946 l -0.6193,2.38125 0.5455,0.92604 c 0.30003,0.50932 1.1764,1.71867 1.9475,2.68743 l 1.44924,1.80272 1.8033728,-0.23533 c 5.72900399,-0.74758 7.6912472,0.523 7.6912472,2.34476 0,2.29921 -4.4984914,2.48899 -8.2760865,-0.16423 -0.8499666,-0.59698 -1.4336605,-1.19001 -1.4336605,-1.19001 0,0 -2.3665326,0.48178 -3.531704,0.79583 -1.202707,0.32417 -1.80274,0.52719 -3.564509,1.12186 0,0 -0.61814,0.89767 -1.02094,1.55026 -1.49858,2.4279 -3.24833,4.43998 -4.49793,5.1723 -1.3991,0.81993 -2.86584,0.87582 -3.60433,0.13733 z m 2.28605,-0.81668 c 0.81883,-0.50607 2.47616,-2.46625 3.62341,-4.28553 l 0.46449,-0.73658 -2.11497,1.06339 c -3.26655,1.64239 -4.76093,3.19033 -3.98386,4.12664 0.43653,0.52598 0.95874,0.48237 2.01093,-0.16792 z m 21.21809,-5.95578 c 0.80089,-0.56097 0.68463,-1.69142 -0.22082,-2.1472 -0.70466,-0.35471 -1.2726074,-0.42759 -3.1031574,-0.40057 -1.1249,0.0767 -2.9337647,0.3034 -3.2403347,0.37237 0,0 0.993716,0.68678 1.434896,0.93922 0.58731,0.33544 2.0145161,0.95811 3.0565161,1.27706 1.02785,0.31461 1.6224,0.28144 2.0729,-0.0409 z m -8.53152,-3.54594 c -0.4847,-0.50952 -1.30889,-1.57296 -1.83152,-2.3632 -0.68353,-0.89643 -1.02629,-1.52887 -1.02629,-1.52887 0,0 -0.4996,1.60694 -0.90948,2.57394 l -1.27876,3.16076 -0.37075,0.71695 c 0,0 1.971043,-0.64627 2.97389,-0.90822 1.0621668,-0.27744 3.21787,-0.70134 3.21787,-0.70134 z m -2.74938,-11.02573 c 0.12363,-1.0375 0.1761,-2.07346 -0.15724,-2.59587 -0.9246,-1.01077 -2.04057,-0.16787 -1.85154,2.23517 0.0636,0.8084 0.26443,2.19033 0.53292,3.04209 l 0.48817,1.54863 0.34358,-1.16638 c 0.18897,-0.64151 0.47882,-2.02015 0.64411,-3.06364 z"
style="fill:#ff2116;fill-opacity:1;stroke-width:0.26458335"
id="path2697" />
<g
id="g858">
<path
inkscape:connector-curvature="0"
id="path845"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:'Franklin Gothic Medium Cond';-inkscape-font-specification:'Franklin Gothic Medium Cond';letter-spacing:0px;word-spacing:4.26000023px;fill:#2c2c2c;fill-opacity:1;stroke:none;stroke-width:0.35824656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -20.930423,167.83862 h 2.364986 q 1.133514,0 1.840213,0.2169 0.706698,0.20991 1.189489,0.9446 0.482795,0.72769 0.482795,1.75625 0,0.94459 -0.391832,1.6233 -0.391833,0.67871 -1.056548,0.97958 -0.65772,0.30087 -2.02913,0.30087 h -0.818651 v 3.72941 h -1.581322 z m 1.581322,1.22447 v 3.33058 h 0.783664 q 1.049552,0 1.44838,-0.39184 0.405826,-0.39183 0.405826,-1.27345 0,-0.65772 -0.265887,-1.06355 -0.265884,-0.41282 -0.587747,-0.50378 -0.314866,-0.098 -1.000572,-0.098 z" />
<path
inkscape:connector-curvature="0"
id="path847"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:'Franklin Gothic Medium Cond';-inkscape-font-specification:'Franklin Gothic Medium Cond';letter-spacing:0px;word-spacing:4.26000023px;fill:#2c2c2c;fill-opacity:1;stroke:none;stroke-width:0.35824656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -13.842461,167.83862 h 2.148082 q 1.560333,0 2.4909318,0.55276 0.9375993,0.55276 1.4133973,1.6443 0.482791,1.09153 0.482791,2.42096 0,1.3994 -0.4338151,2.49793 -0.4268149,1.09153 -1.3154348,1.76324 -0.8816233,0.67172 -2.5189212,0.67172 h -2.267031 z m 1.581326,1.26645 v 7.018 h 0.657715 q 1.378411,0 2.001144,-0.9516 0.6227329,-0.95858 0.6227329,-2.5539 0,-3.5125 -2.6238769,-3.5125 z" />
<path
inkscape:connector-curvature="0"
id="path849"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:'Franklin Gothic Medium Cond';-inkscape-font-specification:'Franklin Gothic Medium Cond';letter-spacing:0px;word-spacing:4.26000023px;fill:#2c2c2c;fill-opacity:1;stroke:none;stroke-width:0.35824656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -5.7889096,167.83862 h 5.30372941 v 1.26645 H -4.2075842 v 2.85478 h 2.9807225 v 1.26646 h -2.9807225 v 4.16322 h -1.5813254 z" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

+28
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using SkiaSharp;
@@ -24,5 +25,32 @@ namespace QuestPDF.ReportSample
Latitude = Helpers.Random.NextDouble() * 180f - 90f
};
}
private static readonly ConcurrentDictionary<int, string> RomanNumeralCache = new ConcurrentDictionary<int, string>();
public static string FormatAsRomanNumeral(this int number)
{
if (number < 0 || number > 3999)
throw new ArgumentOutOfRangeException("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;
});
}
}
}
@@ -26,58 +26,58 @@ namespace QuestPDF.ReportSample.Layouts
private void ComposeHeader(IContainer container)
{
container.Background(Colors.Grey.Lighten3).Border(1).Stack(stack =>
container.Background(Colors.Grey.Lighten3).Border(1).Column(column =>
{
stack.Item().ShowOnce().Padding(5).AlignMiddle().Row(row =>
column.Item().ShowOnce().Padding(5).AlignMiddle().Row(row =>
{
row.RelativeColumn(2).AlignMiddle().Text("PRIMARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
row.RelativeColumn(1).AlignRight().MinimalBox().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30);
row.RelativeItem(2).AlignMiddle().Text("PRIMARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
row.RelativeItem(1).AlignRight().MinimalBox().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30);
});
stack.Item().SkipOnce().Padding(5).Row(row =>
column.Item().SkipOnce().Padding(5).Row(row =>
{
row.RelativeColumn(2).Text("SECONDARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
row.RelativeColumn(1).AlignRight().MinimalBox().Background(Colors.Blue.Lighten4).Padding(15);
row.RelativeItem(2).Text("SECONDARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
row.RelativeItem(1).AlignRight().MinimalBox().Background(Colors.Blue.Lighten4).Padding(15);
});
});
}
private void ComposeContent(IContainer container)
{
container.Stack(stack =>
container.Column(column =>
{
stack.Item().PaddingVertical(80).Text("First");
stack.Item().PageBreak();
stack.Item().PaddingVertical(80).Text("Second");
stack.Item().PageBreak();
stack.Item().PaddingVertical(80).Text("Third");
stack.Item().PageBreak();
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).Stack(stack =>
container.Background(Colors.Grey.Lighten3).Column(column =>
{
stack.Item().ShowOnce().Background(Colors.Grey.Lighten3).Row(row =>
column.Item().ShowOnce().Background(Colors.Grey.Lighten3).Row(row =>
{
row.RelativeColumn().Text(x =>
row.RelativeItem().Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
row.RelativeColumn().AlignRight().Text("Footer for header");
row.RelativeItem().AlignRight().Text("Footer for header");
});
stack.Item().SkipOnce().Background(Colors.Grey.Lighten3).Row(row =>
column.Item().SkipOnce().Background(Colors.Grey.Lighten3).Row(row =>
{
row.RelativeColumn().Text(x =>
row.RelativeItem().Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
row.RelativeColumn().AlignRight().Text("Footer for every page except header");
row.RelativeItem().AlignRight().Text("Footer for every page except header");
});
});
}
@@ -17,11 +17,11 @@ namespace QuestPDF.ReportSample.Layouts
{
container
.ShowEntire()
.Stack(stack =>
.Column(column =>
{
stack.Spacing(5);
stack.Item().Element(PhotoWithMaps);
stack.Item().Element(PhotoDetails);
column.Spacing(5);
column.Item().Element(PhotoWithMaps);
column.Item().Element(PhotoDetails);
});
}
@@ -30,14 +30,14 @@ namespace QuestPDF.ReportSample.Layouts
container
.Row(row =>
{
row.RelativeColumn(2).AspectRatio(4 / 3f).Component<ImagePlaceholder>();
row.RelativeItem(2).AspectRatio(4 / 3f).Component<ImagePlaceholder>();
row.RelativeColumn().PaddingLeft(5).Stack(stack =>
row.RelativeItem().PaddingLeft(5).Column(column =>
{
stack.Spacing(7f);
column.Spacing(7f);
stack.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>();
stack.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>();
column.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>();
column.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>();
});
});
}
@@ -21,25 +21,25 @@ namespace QuestPDF.ReportSample.Layouts
.Decoration(decoration =>
{
decoration
.Header()
.Before()
.PaddingBottom(5)
.Text(Model.Title, Typography.Headline);
decoration.Content().Border(0.75f).BorderColor(Colors.Grey.Medium).Stack(stack =>
decoration.Content().Border(0.75f).BorderColor(Colors.Grey.Medium).Column(column =>
{
foreach (var part in Model.Parts)
{
stack.Item().EnsureSpace(25).Row(row =>
column.Item().EnsureSpace(25).Row(row =>
{
row.ConstantColumn(150).LabelCell().Text(part.Label);
var frame = row.RelativeColumn().ValueCell();
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));
});
@@ -56,12 +56,12 @@ namespace QuestPDF.ReportSample.Layouts
return;
}
container.ShowEntire().Stack(stack =>
container.ShowEntire().Column(column =>
{
stack.Spacing(5);
column.Spacing(5);
stack.Item().MaxWidth(250).AspectRatio(4 / 3f).Component<ImagePlaceholder>();
stack.Item().Text(model.Location.Format());
column.Item().MaxWidth(250).AspectRatio(4 / 3f).Component<ImagePlaceholder>();
column.Item().Text(model.Location.Format());
});
}
+18 -18
View File
@@ -39,28 +39,28 @@ namespace QuestPDF.ReportSample.Layouts
page.Footer().AlignCenter().Text(text =>
{
text.CurrentPageNumber();
text.CurrentPageNumber(format: x => x?.FormatAsRomanNumeral() ?? "-----");
text.Span(" / ");
text.TotalPages();
text.TotalPages(format: x => x?.FormatAsRomanNumeral() ?? "-----");
});
});
}
private void ComposeHeader(IContainer container)
{
container.Stack(stack =>
container.Column(column =>
{
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.Spacing(50);
row.RelativeColumn().PaddingTop(-10).Text(Model.Title, Typography.Title);
row.ConstantColumn(90).ExternalLink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
row.RelativeItem().PaddingTop(-10).Text(Model.Title, Typography.Title);
row.ConstantItem(90).ExternalLink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
});
stack.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal();
column.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal();
stack.Item().ShowOnce().Grid(grid =>
column.Item().ShowOnce().Grid(grid =>
{
grid.Columns(2);
grid.Spacing(5);
@@ -79,22 +79,22 @@ namespace QuestPDF.ReportSample.Layouts
void ComposeContent(IContainer container)
{
container.PaddingVertical(20).Stack(stack =>
container.PaddingVertical(20).Column(column =>
{
stack.Spacing(20);
stack.Item().Component(new TableOfContentsTemplate(Model.Sections));
column.Spacing(20);
stack.Item().PageBreak();
column.Item().Component(new TableOfContentsTemplate(Model.Sections));
column.Item().PageBreak();
foreach (var section in Model.Sections)
stack.Item().Location(section.Title).Component(new SectionTemplate(section));
stack.Item().PageBreak();
stack.Item().Location("Photos");
column.Item().Location(section.Title).Component(new SectionTemplate(section));
column.Item().PageBreak();
column.Item().Location("Photos");
foreach (var photo in Model.Photos)
stack.Item().Component(new PhotoTemplate(photo));
column.Item().Component(new PhotoTemplate(photo));
});
}
}
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.ReportSample.Layouts
@@ -19,18 +21,18 @@ namespace QuestPDF.ReportSample.Layouts
.Decoration(decoration =>
{
decoration
.Header()
.Before()
.PaddingBottom(5)
.Text("Table of contents", Typography.Headline);
decoration.Content().Stack(stack =>
decoration.Content().Column(column =>
{
stack.Spacing(5);
column.Spacing(5);
for (var i = 0; i < Sections.Count; i++)
stack.Item().Element(c => DrawLink(c, i+1, Sections[i].Title));
column.Item().Element(c => DrawLink(c, i+1, Sections[i].Title));
stack.Item().Element(c => DrawLink(c, Sections.Count+1, "Photos"));
column.Item().Element(c => DrawLink(c, Sections.Count+1, "Photos"));
});
});
}
@@ -41,9 +43,20 @@ namespace QuestPDF.ReportSample.Layouts
.InternalLink(locationName)
.Row(row =>
{
row.ConstantColumn(25).Text($"{number}.");
row.RelativeColumn().Text(locationName);
row.ConstantColumn(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName));
row.ConstantItem(25).Text($"{number}.");
row.RelativeItem().Text(locationName);
row.ConstantItem(150).AlignRight().Text(text =>
{
text.BeginPageNumberOfSection(locationName);
text.Span(" - ");
text.EndPageNumberOfSection(locationName);
var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium);
text.Span(" (", lengthStyle);
text.TotalPagesWithinSection(locationName, lengthStyle, x => x == 1 ? "1 page long" : $"{x} pages long");
text.Span(")", lengthStyle);
});
});
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ namespace QuestPDF.ReportSample
[Test]
public void GenerateAndShowPdf()
{
ImagePlaceholder.Solid = true;
//ImagePlaceholder.Solid = true;
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf");
Report.GeneratePdf(path);
@@ -1,4 +1,5 @@
using NUnit.Framework;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Elements;
using QuestPDF.Fluent;
@@ -8,19 +9,40 @@ using QuestPDF.UnitTests.TestEngine;
namespace QuestPDF.UnitTests
{
[TestFixture]
public class StackTests
public class ColumnTests
{
private Column CreateColumnWithTwoItems(TestPlan testPlan)
{
return new Column
{
Items =
{
new ColumnItem
{
Child = testPlan.CreateChild("first")
},
new ColumnItem
{
Child = testPlan.CreateChild("second")
}
}
};
}
private Column CreateColumnWithTwoItemsWhereFirstIsFullyRendered(TestPlan testPlan)
{
var column = CreateColumnWithTwoItems(testPlan);
column.Items.First().IsRendered = true;
return column;
}
#region Measure
[Test]
public void Measure_ReturnsWrap_WhenFirstChildWraps()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.For(CreateColumnWithTwoItems)
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("first", new Size(400, 300), SpacePlan.Wrap())
.CheckMeasureResult(SpacePlan.Wrap());
@@ -30,11 +52,7 @@ namespace QuestPDF.UnitTests
public void Measure_ReturnsPartialRender_WhenFirstChildReturnsPartialRender()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.For(CreateColumnWithTwoItems)
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("first", new Size(400, 300), SpacePlan.PartialRender(300, 200))
.CheckMeasureResult(SpacePlan.PartialRender(300, 200));
@@ -44,11 +62,7 @@ namespace QuestPDF.UnitTests
public void Measure_ReturnsPartialRender_WhenSecondChildWraps()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.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())
@@ -59,11 +73,7 @@ namespace QuestPDF.UnitTests
public void Measure_ReturnsPartialRender_WhenSecondChildReturnsPartialRender()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.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))
@@ -74,33 +84,13 @@ namespace QuestPDF.UnitTests
public void Measure_ReturnsFullRender_WhenSecondChildReturnsFullRender()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.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));
}
[Test]
public void Measure_UsesEmpty_WhenFirstChildIsRendered()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second"),
IsFirstRendered = true
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("second", new Size(400, 300), SpacePlan.FullRender(200, 300))
.CheckMeasureResult(SpacePlan.FullRender(200, 300));
}
#endregion
#region Draw
@@ -109,11 +99,7 @@ namespace QuestPDF.UnitTests
public void Draw_WhenFirstChildWraps()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.For(CreateColumnWithTwoItems)
.DrawElement(new Size(400, 300))
.ExpectChildMeasure("first", new Size(400, 300), SpacePlan.Wrap())
.CheckDrawResult();
@@ -123,14 +109,12 @@ namespace QuestPDF.UnitTests
public void Draw_WhenFirstChildPartiallyRenders()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.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();
}
@@ -138,15 +122,13 @@ namespace QuestPDF.UnitTests
public void Draw_WhenFirstChildFullyRenders_AndSecondChildWraps()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.For(CreateColumnWithTwoItems)
.DrawElement(new Size(400, 300))
.ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100))
.ExpectChildDraw("first", new Size(400, 100))
.ExpectChildMeasure("second", new Size(400, 200), SpacePlan.Wrap())
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw("first", new Size(400, 100))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
@@ -154,15 +136,13 @@ namespace QuestPDF.UnitTests
public void Draw_WhenFirstChildFullyRenders_AndSecondChildPartiallyRenders()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.For(CreateColumnWithTwoItems)
.DrawElement(new Size(400, 300))
.ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100))
.ExpectChildDraw("first", new Size(400, 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)
@@ -173,15 +153,13 @@ namespace QuestPDF.UnitTests
public void Draw_WhenFirstChildFullyRenders_AndSecondChildFullyRenders()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second")
})
.For(CreateColumnWithTwoItems)
.DrawElement(new Size(400, 300))
.ExpectChildMeasure("first", new Size(400, 300), SpacePlan.FullRender(200, 100))
.ExpectChildDraw("first", new Size(400, 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)
@@ -192,19 +170,13 @@ namespace QuestPDF.UnitTests
public void Draw_UsesEmpty_WhenFirstChildIsRendered()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second"),
IsFirstRendered = true
})
.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<BinaryStack>(x => x.IsFirstRendered)
.CheckState<Column>(x => x.Items.First().IsRendered)
.CheckDrawResult();
}
@@ -212,97 +184,14 @@ namespace QuestPDF.UnitTests
public void Draw_TogglesFirstRenderedFlag_WhenSecondFullyRenders()
{
TestPlan
.For(x => new BinaryStack
{
First = x.CreateChild("first"),
Second = x.CreateChild("second"),
IsFirstRendered = true
})
.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<BinaryStack>(x => !x.IsFirstRendered);
}
#endregion
#region Structure
[Test]
public void Structure_Simple()
{
// arrange
var childA = TestPlan.CreateUniqueElement();
var childB = TestPlan.CreateUniqueElement();
var childC = TestPlan.CreateUniqueElement();
var childD = TestPlan.CreateUniqueElement();
var childE = TestPlan.CreateUniqueElement();
const int spacing = 20;
// act
var structure = new Container();
structure.Stack(stack =>
{
stack.Spacing(spacing);
stack.Item().Element(childA);
stack.Item().Element(childB);
stack.Item().Element(childC);
stack.Item().Element(childD);
stack.Item().Element(childE);
});
// assert
var expected = new Padding
{
Bottom = -spacing,
Child = new BinaryStack
{
First = new BinaryStack
{
First = new Padding
{
Bottom = spacing,
Child = childA
},
Second = new Padding
{
Bottom = spacing,
Child = childB
}
},
Second = new BinaryStack
{
First = new Padding
{
Bottom = spacing,
Child = childC
},
Second = new BinaryStack
{
First = new Padding
{
Bottom = spacing,
Child = childD
},
Second = new Padding
{
Bottom = spacing,
Child = childE
}
}
}
}
};
TestPlan.CompareOperations(structure, expected);
.CheckState<Column>(x => !x.Items.First().IsRendered);
}
#endregion
+83 -80
View File
@@ -9,35 +9,27 @@ 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_WhenDecorationReturnsWrap()
public void Measure_ReturnsWrap_WhenBeforeReturnsWrap()
{
TestPlan
.For(x => new BinaryDecoration
{
Type = DecorationType.Append,
DecorationElement = x.CreateChild("decoration"),
ContentElement = x.CreateChild("content")
})
.For(CreateDecoration)
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("decoration", new Size(400, 300), SpacePlan.Wrap())
.CheckMeasureResult(SpacePlan.Wrap());
}
[Test]
public void Measure_ReturnsWrap_WhenDecorationReturnsPartialRender()
{
TestPlan
.For(x => new BinaryDecoration
{
Type = DecorationType.Append,
DecorationElement = x.CreateChild("decoration"),
ContentElement = x.CreateChild("content")
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("decoration", new Size(400, 300), SpacePlan.PartialRender(300, 200))
.ExpectChildMeasure("before", new Size(400, 300), SpacePlan.Wrap())
.ExpectChildMeasure("after", new Size(400, 300), SpacePlan.FullRender(100, 50))
.ExpectChildMeasure("content", new Size(400, 250), SpacePlan.FullRender(100, 100))
.CheckMeasureResult(SpacePlan.Wrap());
}
@@ -45,89 +37,100 @@ namespace QuestPDF.UnitTests
public void Measure_ReturnsWrap_WhenContentReturnsWrap()
{
TestPlan
.For(x => new BinaryDecoration
{
Type = DecorationType.Append,
DecorationElement = x.CreateChild("decoration"),
ContentElement = x.CreateChild("content")
})
.For(CreateDecoration)
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("decoration", new Size(400, 300), SpacePlan.FullRender(300, 100))
.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())
.CheckMeasureResult(SpacePlan.Wrap());
}
[Test]
public void Measure_ReturnsPartialRender_WhenContentReturnsPartialRender()
public void Measure_ReturnsWrap_WhenAfterReturnsWrap()
{
TestPlan
.For(x => new BinaryDecoration
{
Type = DecorationType.Append,
DecorationElement = x.CreateChild("decoration"),
ContentElement = x.CreateChild("content")
})
.For(CreateDecoration)
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("decoration", new Size(400, 300), SpacePlan.FullRender(300, 100))
.ExpectChildMeasure("content", new Size(400, 200), SpacePlan.PartialRender(200, 150))
.CheckMeasureResult(SpacePlan.PartialRender(400, 250));
.ExpectChildMeasure("before", new Size(400, 300), SpacePlan.FullRender(100, 50))
.ExpectChildMeasure("after", new Size(400, 300), SpacePlan.Wrap())
.ExpectChildMeasure("content", new Size(400, 250), SpacePlan.FullRender(100, 100))
.CheckMeasureResult(SpacePlan.Wrap());
}
[Test]
public void Measure_ReturnsFullRender_WhenContentReturnsFullRender()
public void Measure_ReturnsWrap_WhenBeforeReturnsPartialRender()
{
TestPlan
.For(x => new BinaryDecoration
{
Type = DecorationType.Append,
DecorationElement = x.CreateChild("decoration"),
ContentElement = x.CreateChild("content")
})
.For(CreateDecoration)
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("decoration", new Size(400, 300), SpacePlan.FullRender(300, 100))
.ExpectChildMeasure("content", new Size(400, 200), SpacePlan.FullRender(200, 150))
.CheckMeasureResult(SpacePlan.FullRender(400, 250));
.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());
}
[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());
}
[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_Prepend()
{
TestPlan
.For(x => new BinaryDecoration
{
Type = DecorationType.Prepend,
DecorationElement = x.CreateChild("decoration"),
ContentElement = x.CreateChild("content")
})
.DrawElement(new Size(400, 300))
.ExpectChildMeasure("decoration", new Size(400, 300), SpacePlan.FullRender(300, 100))
.ExpectChildDraw("decoration", new Size(400, 100))
.ExpectCanvasTranslate(0, 100)
.ExpectChildDraw("content", new Size(400, 200))
.ExpectCanvasTranslate(0, -100)
.CheckDrawResult();
}
[Test]
public void Draw_Append()
{
TestPlan
.For(x => new BinaryDecoration
{
Type = DecorationType.Append,
DecorationElement = x.CreateChild("decoration"),
ContentElement = x.CreateChild("content")
})
.For(CreateDecoration)
.DrawElement(new Size(400, 300))
.ExpectChildMeasure("decoration", new Size(400, 300), SpacePlan.FullRender(300, 100))
.ExpectChildDraw("content", new Size(400, 200))
.ExpectCanvasTranslate(0, 200)
.ExpectChildDraw("decoration", new Size(400, 100))
.ExpectCanvasTranslate(0, -200)
.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();
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
public class ExternalLinkTests
{
[Test]
public void Measure() => SimpleContainerTests.Measure<ExternalLink>();
public void Measure() => SimpleContainerTests.Measure<Hyperlink>();
// TODO: consider tests for the Draw method
}
+146
View File
@@ -0,0 +1,146 @@
using FluentAssertions;
using NUnit.Framework;
using QuestPDF.Drawing;
using SkiaSharp;
using static SkiaSharp.SKFontStyleSlant;
namespace QuestPDF.UnitTests
{
[TestFixture]
public class FontStyleSetTests
{
private void ExpectComparisonOrder(SKFontStyle target, SKFontStyle[] styles)
{
for (var i = 0; i < styles.Length - 1; i++)
{
var currentStyle = styles[i];
var nextStyle = styles[i + 1];
FontStyleSet.IsBetterMatch(target, currentStyle, nextStyle).Should().BeTrue();
FontStyleSet.IsBetterMatch(target, nextStyle, currentStyle).Should().BeFalse();
}
}
[Test]
public void FontStyleSet_IsBetterMatch_CondensedWidth()
{
var styles = new[]
{
new SKFontStyle(500, 5, Upright),
new SKFontStyle(500, 4, Upright),
new SKFontStyle(500, 3, Upright),
new SKFontStyle(500, 6, Upright)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_ExpandedWidth()
{
var styles = new[]
{
new SKFontStyle(500, 6, Upright),
new SKFontStyle(500, 7, Upright),
new SKFontStyle(500, 8, Upright),
new SKFontStyle(500, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(500, 6, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_ItalicSlant()
{
var styles = new[]
{
new SKFontStyle(500, 5, Italic),
new SKFontStyle(500, 5, Oblique),
new SKFontStyle(500, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Italic), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_ObliqueSlant()
{
var styles = new[]
{
new SKFontStyle(500, 5, Oblique),
new SKFontStyle(500, 5, Italic),
new SKFontStyle(500, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Oblique), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_UprightSlant()
{
var styles = new[]
{
new SKFontStyle(500, 5, Upright),
new SKFontStyle(500, 5, Oblique),
new SKFontStyle(500, 5, Italic)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_ThinWeight()
{
var styles = new[]
{
new SKFontStyle(300, 5, Upright),
new SKFontStyle(200, 5, Upright),
new SKFontStyle(100, 5, Upright),
new SKFontStyle(400, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(300, 5, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_RegularWeight()
{
var styles = new[]
{
new SKFontStyle(500, 5, Upright),
new SKFontStyle(300, 5, Upright),
new SKFontStyle(100, 5, Upright),
new SKFontStyle(600, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(400, 5, Upright), styles);
}
[Test]
public void FontStyleSet_IsBetterMatch_BoldWeight()
{
var styles = new[]
{
new SKFontStyle(600, 5, Upright),
new SKFontStyle(700, 5, Upright),
new SKFontStyle(800, 5, Upright),
new SKFontStyle(500, 5, Upright)
};
ExpectComparisonOrder(new SKFontStyle(600, 5, Upright), styles);
}
[Test]
public void FontStyleSet_RespectsPriority()
{
var styles = new[]
{
new SKFontStyle(600, 5, Italic),
new SKFontStyle(600, 6, Upright),
new SKFontStyle(500, 6, Italic)
};
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
}
}
}
+54 -54
View File
@@ -39,26 +39,26 @@ namespace QuestPDF.UnitTests
// assert
var expected = new Container();
expected.Stack(stack =>
expected.Column(column =>
{
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(6).Element(childA);
row.RelativeColumn(4).Element(childB);
row.RelativeColumn(2);
row.RelativeItem(6).Element(childA);
row.RelativeItem(4).Element(childB);
row.RelativeItem(2);
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(4).Element(childC);
row.RelativeColumn(2).Element(childD);
row.RelativeColumn(6);
row.RelativeItem(4).Element(childC);
row.RelativeItem(2).Element(childD);
row.RelativeItem(6);
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(8).Element(childE);
row.RelativeColumn(4);
row.RelativeItem(8).Element(childE);
row.RelativeItem(4);
});
});
@@ -93,29 +93,29 @@ namespace QuestPDF.UnitTests
// assert
var expected = new Container();
expected.Stack(stack =>
expected.Column(column =>
{
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(1);
row.RelativeColumn(6).Element(childA);
row.RelativeColumn(4).Element(childB);
row.RelativeColumn(1);
row.RelativeItem(1);
row.RelativeItem(6).Element(childA);
row.RelativeItem(4).Element(childB);
row.RelativeItem(1);
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(3);
row.RelativeColumn(4).Element(childC);
row.RelativeColumn(2).Element(childD);
row.RelativeColumn(3);
row.RelativeItem(3);
row.RelativeItem(4).Element(childC);
row.RelativeItem(2).Element(childD);
row.RelativeItem(3);
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(2);
row.RelativeColumn(8).Element(childE);
row.RelativeColumn(2);
row.RelativeItem(2);
row.RelativeItem(8).Element(childE);
row.RelativeItem(2);
});
});
@@ -150,26 +150,26 @@ namespace QuestPDF.UnitTests
// assert
var expected = new Container();
expected.Stack(stack =>
expected.Column(column =>
{
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(2);
row.RelativeColumn(6).Element(childA);
row.RelativeColumn(4).Element(childB);
row.RelativeItem(2);
row.RelativeItem(6).Element(childA);
row.RelativeItem(4).Element(childB);
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(6);
row.RelativeColumn(4).Element(childC);
row.RelativeColumn(2).Element(childD);
row.RelativeItem(6);
row.RelativeItem(4).Element(childC);
row.RelativeItem(2).Element(childD);
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.RelativeColumn(4);
row.RelativeColumn(8).Element(childE);
row.RelativeItem(4);
row.RelativeItem(8).Element(childE);
});
});
@@ -210,36 +210,36 @@ namespace QuestPDF.UnitTests
// assert
var expected = new Container();
expected.Stack(stack =>
expected.Column(column =>
{
stack.Spacing(20);
column.Spacing(20);
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.Spacing(30);
row.RelativeColumn(3);
row.RelativeColumn(5).Element(childA);
row.RelativeColumn(5).Element(childB);
row.RelativeColumn(3);
row.RelativeItem(3);
row.RelativeItem(5).Element(childA);
row.RelativeItem(5).Element(childB);
row.RelativeItem(3);
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.Spacing(30);
row.RelativeColumn(3);
row.RelativeColumn(10).Element(childC);
row.RelativeColumn(3);
row.RelativeItem(3);
row.RelativeItem(10).Element(childC);
row.RelativeItem(3);
});
stack.Item().Row(row =>
column.Item().Row(row =>
{
row.Spacing(30);
row.RelativeColumn(2);
row.RelativeColumn(12).Element(childD);
row.RelativeColumn(2);
row.RelativeItem(2);
row.RelativeItem(12).Element(childD);
row.RelativeItem(2);
});
});
+1 -1
View File
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
public class InternalLinkTests
{
[Test]
public void Measure() => SimpleContainerTests.Measure<InternalLink>();
public void Measure() => SimpleContainerTests.Measure<SectionLink>();
// TODO: consider tests for the Draw method
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
public class InternalLocationTests
{
[Test]
public void Measure() => SimpleContainerTests.Measure<InternalLink>();
public void Measure() => SimpleContainerTests.Measure<SectionLink>();
// TODO: consider tests for the Draw method
}
-267
View File
@@ -1,267 +0,0 @@
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 RowTests
{
#region Measure
[Test]
public void Measure_ReturnsWrap_WhenLeftChildReturnsWrap()
{
TestPlan
.For(x => new BinaryRow
{
Left = x.CreateChild("left"),
Right = x.CreateChild("right")
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("left", new Size(400, 300), SpacePlan.Wrap())
.CheckMeasureResult(SpacePlan.Wrap());
}
[Test]
public void Measure_ReturnsWrap_WhenRightChildReturnsWrap()
{
TestPlan
.For(x => new BinaryRow
{
Left = x.CreateChild("left"),
Right = x.CreateChild("right")
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("left", new Size(400, 300), SpacePlan.FullRender(250, 150))
.ExpectChildMeasure("right", new Size(150, 300), SpacePlan.Wrap())
.CheckMeasureResult(SpacePlan.Wrap());
}
[Test]
public void Measure_ReturnsPartialRender_WhenLeftChildReturnsPartialRender()
{
TestPlan
.For(x => new BinaryRow
{
Left = x.CreateChild("left"),
Right = x.CreateChild("right")
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("left", new Size(400, 300), SpacePlan.PartialRender(250, 150))
.ExpectChildMeasure("right", new Size(150, 300), SpacePlan.FullRender(100, 100))
.CheckMeasureResult(SpacePlan.PartialRender(350, 150));
}
[Test]
public void Measure_ReturnsPartialRender_WhenRightChildReturnsPartialRender()
{
TestPlan
.For(x => new BinaryRow
{
Left = x.CreateChild("left"),
Right = x.CreateChild("right")
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("left", new Size(400, 300), SpacePlan.FullRender(250, 150))
.ExpectChildMeasure("right", new Size(150, 300), SpacePlan.PartialRender(100, 100))
.CheckMeasureResult(SpacePlan.PartialRender(350, 150));
}
[Test]
public void Measure_ReturnsFullRender_WhenBothChildrenReturnFullRender()
{
TestPlan
.For(x => new BinaryRow
{
Left = x.CreateChild("left"),
Right = x.CreateChild("right")
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure("left", new Size(400, 300), SpacePlan.FullRender(200, 150))
.ExpectChildMeasure("right", new Size(200, 300), SpacePlan.FullRender(100, 100))
.CheckMeasureResult(SpacePlan.FullRender(300, 150));
}
#endregion
#region Draw
[Test]
public void Draw()
{
TestPlan
.For(x => new BinaryRow
{
Left = x.CreateChild("left"),
Right = x.CreateChild("right")
})
.DrawElement(new Size(400, 300))
.ExpectChildMeasure("left", new Size(400, 300), SpacePlan.FullRender(250, 150))
.ExpectChildDraw("left", new Size(250, 300))
.ExpectCanvasTranslate(250, 0)
.ExpectChildDraw("right", new Size(150, 300))
.ExpectCanvasTranslate(-250, 0)
.CheckDrawResult();
}
#endregion
#region Structure
[Test]
public void Structure_RelativeColumnsHandling()
{
// arrange
var childA = TestPlan.CreateUniqueElement();
var childB = TestPlan.CreateUniqueElement();
var childC = TestPlan.CreateUniqueElement();
var childD = TestPlan.CreateUniqueElement();
var childE = TestPlan.CreateUniqueElement();
const int spacing = 25;
var availableSpace = new Size(1100, 400);
// act
var value = new Container();
value.Row(row =>
{
row.Spacing(spacing);
row.ConstantColumn(150).Element(childA);
row.ConstantColumn(250).Element(childB);
row.RelativeColumn(1).Element(childC);
row.RelativeColumn(2).Element(childD);
row.RelativeColumn(3).Element(childE);
});
// assert
var expected = new Container();
expected.Row(row =>
{
row.Spacing(spacing);
row.ConstantColumn(150).Element(childA);
row.ConstantColumn(250).Element(childB);
row.ConstantColumn(100).Element(childC);
row.ConstantColumn(200).Element(childD);
row.ConstantColumn(300).Element(childE);
});
TestPlan.CompareOperations(value, expected, availableSpace);
}
[Test]
public void Structure_Tree()
{
// arrange
var childA = TestPlan.CreateUniqueElement();
var childB = TestPlan.CreateUniqueElement();
var childC = TestPlan.CreateUniqueElement();
var childD = TestPlan.CreateUniqueElement();
var childE = TestPlan.CreateUniqueElement();
const int spacing = 25;
var availableSpace = new Size(1200, 400);
// act
var value = new Container();
value.Row(row =>
{
row.Spacing(spacing);
row.ConstantColumn(150).Element(childA);
row.ConstantColumn(200).Element(childB);
row.ConstantColumn(250).Element(childC);
row.RelativeColumn(2).Element(childD);
row.RelativeColumn(3).Element(childE);
});
// assert
var expected = new BinaryRow
{
Left = new BinaryRow
{
Left = new BinaryRow
{
Left = new Constrained
{
MinWidth = 150,
MaxWidth = 150,
Child = childA
},
Right = new Constrained
{
MinWidth = 25,
MaxWidth = 25
}
},
Right = new BinaryRow
{
Left = new Constrained
{
MinWidth = 200,
MaxWidth = 200,
Child = childB
},
Right = new Constrained
{
MinWidth = 25,
MaxWidth = 25
}
}
},
Right = new BinaryRow
{
Left = new BinaryRow
{
Left = new Constrained
{
MinWidth = 250,
MaxWidth = 250,
Child = childC
},
Right = new Constrained
{
MinWidth = 25,
MaxWidth = 25
}
},
Right = new BinaryRow
{
Left = new Constrained
{
MinWidth = 200,
MaxWidth = 200,
Child = childD
},
Right = new BinaryRow
{
Left = new Constrained
{
MinWidth = 25,
MaxWidth = 25
},
Right = new Constrained
{
MinWidth = 300,
MaxWidth = 300,
Child = childE
}
}
}
}
};
TestPlan.CompareOperations(value, expected, availableSpace);
}
#endregion
}
}
+5 -4
View File
@@ -19,10 +19,11 @@ namespace QuestPDF.UnitTests.TestEngine
public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
public void DrawShapedText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
public void DrawLocation(string locationName) => throw new NotImplementedException();
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
public void DrawSection(string sectionName) => throw new NotImplementedException();
}
}
@@ -16,10 +16,11 @@ namespace QuestPDF.UnitTests.TestEngine
public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
public void DrawShapedText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
public void DrawLocation(string locationName) => throw new NotImplementedException();
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
public void DrawSection(string sectionName) => throw new NotImplementedException();
}
}
+3 -3
View File
@@ -16,13 +16,13 @@ namespace QuestPDF.Drawing
var container = new Container();
container
.Stack(stack =>
.Column(column =>
{
Pages
.SelectMany(x => new List<Action>()
{
() => stack.Item().PageBreak(),
() => stack.Item().Component(x)
() => column.Item().PageBreak(),
() => column.Item().Component(x)
})
.Skip(1)
.ToList()
+4 -2
View File
@@ -18,14 +18,16 @@ namespace QuestPDF.Drawing
internal static void GeneratePdf(Stream stream, IDocument document)
{
var metadata = document.GetMetadata();
var canvas = new PdfCanvas(stream, metadata);
var writeOnlyStream = new WriteOnlyStream(stream);
var canvas = new PdfCanvas(writeOnlyStream, metadata);
RenderDocument(canvas, document);
}
internal static void GenerateXps(Stream stream, IDocument document)
{
var metadata = document.GetMetadata();
var canvas = new XpsCanvas(stream, metadata);
var writeOnlyStream = new WriteOnlyStream(stream);
var canvas = new XpsCanvas(writeOnlyStream, metadata);
RenderDocument(canvas, document);
}
+66 -17
View File
@@ -1,23 +1,52 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
public static class FontManager
{
private static ConcurrentDictionary<string, SKTypeface> Typefaces = new ConcurrentDictionary<string, SKTypeface>();
private static ConcurrentDictionary<string, SKFontMetrics> FontMetrics = new ConcurrentDictionary<string, SKFontMetrics>();
private static ConcurrentDictionary<string, SKPaint> Paints = new ConcurrentDictionary<string, SKPaint>();
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new ConcurrentDictionary<string, SKPaint>();
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
private static ConcurrentDictionary<object, SKPaint> Paints = new();
private static ConcurrentDictionary<object, SKFont> Fonts = new();
private static ConcurrentDictionary<object, SKShaper> Shapers = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
private static void RegisterFontType(SKData fontData, string? customName = null)
{
foreach (var index in Enumerable.Range(0, 256))
{
var typeface = SKTypeface.FromData(fontData, index);
if (typeface == null)
break;
var typefaceName = customName ?? typeface.FamilyName;
var fontStyleSet = StyleSets.GetOrAdd(typefaceName, _ => new FontStyleSet());
fontStyleSet.Add(typeface);
}
}
[Obsolete("Since version 2022.3, the FontManager class offers better font type matching support. Please use the RegisterFontType(Stream stream) overload.")]
public static void RegisterFontType(string fontName, Stream stream)
{
Typefaces.TryAdd(fontName, SKTypeface.FromStream(stream));
using var fontData = SKData.Create(stream);
RegisterFontType(fontData);
RegisterFontType(fontData, customName: fontName);
}
public static void RegisterFontType(Stream stream)
{
using var fontData = SKData.Create(stream);
RegisterFontType(fontData);
}
internal static SKPaint ColorToPaint(this string color)
{
return ColorPaint.GetOrAdd(color, Convert);
@@ -30,37 +59,57 @@ namespace QuestPDF.Drawing
};
}
}
internal static SKPaint ToPaint(this TextStyle style)
{
return Paints.GetOrAdd(style.Key, key => Convert(style));
return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
static SKPaint Convert(TextStyle style)
{
return new SKPaint
{
Color = SKColor.Parse(style.Color),
Typeface = GetTypeface(style),
TextSize = (style.Size ?? 12),
TextEncoding = SKTextEncoding.Utf32
TextSize = style.Size ?? 12
};
}
static SKTypeface GetTypeface(TextStyle style)
{
if (Typefaces.TryGetValue(style.FontType, out var result))
return result;
var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
if (StyleSets.TryGetValue(style.FontType, out var fontStyleSet))
return fontStyleSet.Match(fontStyle);
var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontType, fontStyle);
return SKTypeface.FromFamilyName(style.FontType, (int)(style.FontWeight ?? FontWeight.Normal), (int)SKFontStyleWidth.Normal, slant)
?? throw new ArgumentException($"The typeface {style.FontType} could not be found.");
if (fontFromDefaultSource != null)
return fontFromDefaultSource;
throw new ArgumentException(
$"The typeface '{style.FontType}' could not be found. " +
$"Please consider the following options: " +
$"1) install the font on your operating system or execution environment. " +
$"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method.");
}
}
internal static SKFont ToFont(this TextStyle style)
{
return Fonts.GetOrAdd(style.FontMetricsKey, _ => style.ToPaint().Typeface.ToFont());
}
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
{
return FontMetrics.GetOrAdd(style.Key, key => style.ToPaint().FontMetrics);
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
}
internal static SKShaper ToShaper(this TextStyle style)
{
return Shapers.GetOrAdd(style.FontMetricsKey, _ => new SKShaper(style.ToFont().Typeface));
}
}
}
+132
View File
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using SkiaSharp;
namespace QuestPDF.Drawing
{
internal class FontStyleSet
{
private ConcurrentDictionary<SKFontStyle, SKTypeface> Styles { get; } = new();
public void Add(SKTypeface typeface)
{
var style = typeface.FontStyle;
Styles.AddOrUpdate(style, _ => typeface, (_, _) => typeface);
}
public SKTypeface? Match(SKFontStyle target)
{
SKFontStyle? bestStyle = null;
SKTypeface? bestTypeface = null;
foreach (var entry in Styles)
{
if (IsBetterMatch(target, entry.Key, bestStyle))
{
bestStyle = entry.Key;
bestTypeface = entry.Value;
}
}
return bestTypeface;
}
private static Dictionary<SKFontStyleSlant, List<SKFontStyleSlant>> SlantFallbacks = new()
{
{ SKFontStyleSlant.Italic, new() { SKFontStyleSlant.Italic, SKFontStyleSlant.Oblique, SKFontStyleSlant.Upright } },
{ SKFontStyleSlant.Oblique, new() { SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic, SKFontStyleSlant.Upright } },
{ SKFontStyleSlant.Upright, new() { SKFontStyleSlant.Upright, SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic } },
};
// Checks whether style a is a better match for the target then style b. Uses the CSS font style matching algorithm
internal static bool IsBetterMatch(SKFontStyle? target, SKFontStyle? a, SKFontStyle? b)
{
// A font is better than no font
if (b == null)
return true;
if (a == null)
return false;
// First check font width
// For normal and condensed widths prefer smaller widths
// For expanded widths prefer larger widths
if (target.Width <= (int)SKFontStyleWidth.Normal)
{
if (a.Width <= target.Width && b.Width > target.Width)
return true;
if (a.Width > target.Width && b.Width <= target.Width)
return false;
}
else
{
if (a.Width >= target.Width && b.Width < target.Width)
return true;
if (a.Width < target.Width && b.Width >= target.Width)
return false;
}
// Prefer closest match
var widthDifferenceA = Math.Abs(a.Width - target.Width);
var widthDifferenceB = Math.Abs(b.Width - target.Width);
if (widthDifferenceA < widthDifferenceB)
return true;
if (widthDifferenceB < widthDifferenceA)
return false;
// Prefer closest slant based on provided fallback list
var slantFallback = SlantFallbacks[target.Slant];
var slantIndexA = slantFallback.IndexOf(a.Slant);
var slantIndexB = slantFallback.IndexOf(b.Slant);
if (slantIndexA < slantIndexB)
return true;
if (slantIndexB < slantIndexA)
return false;
// Check weight last
// For thin (<400) weights, prefer thinner weights
// For regular (400-500) weights, prefer other regular weights, then use rule for thin or bold
// For bold (>500) weights, prefer thicker weights
// Behavior for values other than multiples of 100 is not given in the specification
if (target.Weight >= 400 && target.Weight <= 500)
{
if ((a.Weight >= 400 && a.Weight <= 500) && !(b.Weight >= 400 && b.Weight <= 500))
return true;
if (!(a.Weight >= 400 && a.Weight <= 500) && (b.Weight >= 400 && b.Weight <= 500))
return false;
}
if (target.Weight < 450)
{
if (a.Weight <= target.Weight && b.Weight > target.Weight)
return true;
if (a.Weight > target.Weight && b.Weight <= target.Weight)
return false;
}
else
{
if (a.Weight >= target.Weight && b.Weight < target.Weight)
return true;
if (a.Weight < target.Weight && b.Weight >= target.Weight)
return false;
}
// Prefer closest weight
var weightDifferenceA = Math.Abs(a.Weight - target.Weight);
var weightDifferenceB = Math.Abs(b.Weight - target.Weight);
return weightDifferenceA < weightDifferenceB;
}
}
}
+8 -3
View File
@@ -44,6 +44,11 @@ namespace QuestPDF.Drawing
public void DrawText(string text, Position position, TextStyle style)
{
}
public void DrawShapedText(string text, Position position, TextStyle style)
{
}
public void DrawImage(SKImage image, Position position, Size size)
@@ -51,17 +56,17 @@ namespace QuestPDF.Drawing
}
public void DrawExternalLink(string url, Size size)
public void DrawHyperlink(string url, Size size)
{
}
public void DrawLocationLink(string locationName, Size size)
public void DrawSectionLink(string sectionName, Size size)
{
}
public void DrawLocation(string locationName)
public void DrawSection(string sectionName)
{
}
+1 -1
View File
@@ -1,5 +1,5 @@
using System.IO;
using QuestPDF.Infrastructure;
using QuestPDF.Helpers;
using SkiaSharp;
namespace QuestPDF.Drawing
+11 -5
View File
@@ -1,5 +1,6 @@
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
@@ -32,24 +33,29 @@ namespace QuestPDF.Drawing
Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
}
public void DrawShapedText(string text, Position vector, TextStyle style)
{
Canvas.DrawShapedText(text, vector.X, vector.Y, style.ToPaint());
}
public void DrawImage(SKImage image, Position vector, Size size)
{
Canvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height));
}
public void DrawExternalLink(string url, Size size)
public void DrawHyperlink(string url, Size size)
{
Canvas.DrawUrlAnnotation(new SKRect(0, 0, size.Width, size.Height), url);
}
public void DrawLocationLink(string locationName, Size size)
public void DrawSectionLink(string sectionName, Size size)
{
Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), locationName);
Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), sectionName);
}
public void DrawLocation(string locationName)
public void DrawSection(string sectionName)
{
Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), locationName);
Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), sectionName);
}
public void Rotate(float angle)
+1 -1
View File
@@ -1,5 +1,5 @@
using System.IO;
using QuestPDF.Infrastructure;
using QuestPDF.Helpers;
using SkiaSharp;
namespace QuestPDF.Drawing
+123
View File
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ColumnItem : Container
{
public bool IsRendered { get; set; }
}
internal class ColumnItemRenderingCommand
{
public ColumnItem ColumnItem { get; set; }
public SpacePlan Measurement { get; set; }
public Size Size { get; set; }
public Position Offset { get; set; }
}
internal class Column : Element, ICacheable, IStateResettable
{
internal List<ColumnItem> Items { get; } = new();
internal float Spacing { get; set; }
public void ResetState()
{
Items.ForEach(x => x.IsRendered = false);
}
internal override IEnumerable<Element?> GetChildren()
{
return Items;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Items.ForEach(x => x.Child = create(x.Child));
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!Items.Any())
return SpacePlan.FullRender(Size.Zero);
var renderingCommands = PlanLayout(availableSpace);
if (!renderingCommands.Any())
return SpacePlan.Wrap();
var width = renderingCommands.Max(x => x.Size.Width);
var height = renderingCommands.Last().Offset.Y + renderingCommands.Last().Size.Height;
var size = new Size(width, height);
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap();
var totalRenderedItems = Items.Count(x => x.IsRendered) + renderingCommands.Count(x => x.Measurement.Type == 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)
{
if (command.Measurement.Type == SpacePlanType.FullRender)
command.ColumnItem.IsRendered = true;
var targetSize = new Size(availableSpace.Width, command.Size.Height);
Canvas.Translate(command.Offset);
command.ColumnItem.Draw(targetSize);
Canvas.Translate(command.Offset.Reverse());
}
if (Items.All(x => x.IsRendered))
ResetState();
}
private ICollection<ColumnItemRenderingCommand> PlanLayout(Size availableSpace)
{
var topOffset = 0f;
var commands = new List<ColumnItemRenderingCommand>();
foreach (var item in Items)
{
if (item.IsRendered)
continue;
var itemSpace = new Size(availableSpace.Width, availableSpace.Height - topOffset);
var measurement = item.Measure(itemSpace);
if (measurement.Type == SpacePlanType.Wrap)
break;
commands.Add(new ColumnItemRenderingCommand
{
ColumnItem = item,
Size = measurement,
Measurement = measurement,
Offset = new Position(0, topOffset)
});
if (measurement.Type == SpacePlanType.PartialRender)
break;
topOffset += measurement.Height + Spacing;
}
var targetWidth = commands.Select(x => x.Size.Width).DefaultIfEmpty(0).Max();
commands.ForEach(x => x.Size = new Size(targetWidth, x.Size.Height));
return commands;
}
}
}
+78 -64
View File
@@ -1,98 +1,112 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal enum DecorationType
internal class DecorationItemRenderingCommand
{
Prepend,
Append
public Element Element { get; set; }
public SpacePlan Measurement { get; set; }
public Position Offset { get; set; }
}
internal class BinaryDecoration : Element, ICacheable
internal class Decoration : Element, ICacheable
{
public Element DecorationElement { get; set; } = Empty.Instance;
public Element ContentElement { get; set; } = Empty.Instance;
public DecorationType Type { get; set; }
internal Element Before { get; set; } = new Empty();
internal Element Content { get; set; } = new Empty();
internal Element After { get; set; } = new Empty();
internal override IEnumerable<Element?> GetChildren()
{
yield return DecorationElement;
yield return ContentElement;
yield return Before;
yield return Content;
yield return After;
}
internal override void CreateProxy(Func<Element, Element> create)
internal override void CreateProxy(Func<Element?, Element?> create)
{
DecorationElement = create(DecorationElement);
ContentElement = create(ContentElement);
Before = create(Before);
Content = create(Content);
After = create(After);
}
internal override SpacePlan Measure(Size availableSpace)
{
var decorationMeasure = DecorationElement.Measure(availableSpace);
if (decorationMeasure.Type == SpacePlanType.Wrap || decorationMeasure.Type == SpacePlanType.PartialRender)
var renderingCommands = PlanLayout(availableSpace).ToList();
if (renderingCommands.Any(x => x.Measurement.Type == SpacePlanType.Wrap))
return SpacePlan.Wrap();
var decorationSize = decorationMeasure;
var contentMeasure = ContentElement.Measure(new Size(availableSpace.Width, availableSpace.Height - decorationSize.Height));
var width = renderingCommands.Max(x => x.Measurement.Width);
var height = renderingCommands.Sum(x => x.Measurement.Height);
var size = new Size(width, height);
if (contentMeasure.Type == SpacePlanType.Wrap)
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap();
var willBeFullyRendered = renderingCommands.All(x => x.Measurement.Type == SpacePlanType.FullRender);
var contentSize = contentMeasure;
var resultSize = new Size(availableSpace.Width, decorationSize.Height + contentSize.Height);
if (contentSize.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(resultSize);
if (contentSize.Type == SpacePlanType.FullRender)
return SpacePlan.FullRender(resultSize);
throw new NotSupportedException();
return willBeFullyRendered
? SpacePlan.FullRender(size)
: SpacePlan.PartialRender(size);
}
internal override void Draw(Size availableSpace)
{
var decorationSize = DecorationElement.Measure(availableSpace);
var contentSize = new Size(availableSpace.Width, availableSpace.Height - decorationSize.Height);
var translateHeight = Type == DecorationType.Prepend ? decorationSize.Height : contentSize.Height;
Action drawDecoration = () => DecorationElement?.Draw(new Size(availableSpace.Width, decorationSize.Height));
Action drawContent = () => ContentElement?.Draw(new Size (availableSpace.Width, contentSize.Height));
var first = Type == DecorationType.Prepend ? drawDecoration : drawContent;
var second = Type == DecorationType.Prepend ? drawContent : drawDecoration;
first();
Canvas.Translate(new Position(0, translateHeight));
second();
Canvas.Translate(new Position(0, -translateHeight));
}
}
internal class Decoration : IComponent
{
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)
{
container.Element(new BinaryDecoration
var renderingCommands = PlanLayout(availableSpace).ToList();
var width = renderingCommands.Max(x => x.Measurement.Width);
foreach (var command in renderingCommands)
{
Type = DecorationType.Prepend,
DecorationElement = Header,
ContentElement = new BinaryDecoration
{
Type = DecorationType.Append,
ContentElement = Content,
DecorationElement = Footer
}
});
var elementSize = new Size(width, command.Measurement.Height);
Canvas.Translate(command.Offset);
command.Element.Draw(elementSize);
Canvas.Translate(command.Offset.Reverse());
}
}
private IEnumerable<DecorationItemRenderingCommand> PlanLayout(Size availableSpace)
{
SpacePlan GetDecorationMeasurement(Element element)
{
var measurement = element.Measure(availableSpace);
return measurement.Type == SpacePlanType.FullRender
? measurement
: SpacePlan.Wrap();
}
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);
yield return new DecorationItemRenderingCommand
{
Element = Before,
Measurement = beforeMeasurement,
Offset = Position.Zero
};
yield return new DecorationItemRenderingCommand
{
Element = Content,
Measurement = contentMeasurement,
Offset = new Position(0, beforeMeasurement.Height)
};
yield return new DecorationItemRenderingCommand
{
Element = After,
Measurement = afterMeasurement,
Offset = new Position(0, beforeMeasurement.Height + contentMeasurement.Height)
};
}
}
}
+6 -6
View File
@@ -27,12 +27,12 @@ namespace QuestPDF.Elements
{
ChildrenQueue = new Queue<GridElement>(Children);
container.Stack(stack =>
container.Column(column =>
{
stack.Spacing(VerticalSpacing);
column.Spacing(VerticalSpacing);
while (ChildrenQueue.Any())
stack.Item().Row(BuildRow);
column.Item().Row(BuildRow);
});
}
@@ -65,12 +65,12 @@ namespace QuestPDF.Elements
emptySpace /= 2;
if (hasEmptySpace && Alignment != HorizontalAlignment.Left)
row.RelativeColumn(emptySpace);
row.RelativeItem(emptySpace);
elements.ForEach(x => row.RelativeColumn(x.Columns).Element(x.Child));
elements.ForEach(x => row.RelativeItem(x.Columns).Element(x.Child));
if (hasEmptySpace && Alignment != HorizontalAlignment.Right)
row.RelativeColumn(emptySpace);
row.RelativeItem(emptySpace);
}
}
}
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ExternalLink : ContainerElement
internal class Hyperlink : ContainerElement
{
public string Url { get; set; } = "https://www.questpdf.com";
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
if (targetSize.Type == SpacePlanType.Wrap)
return;
Canvas.DrawExternalLink(Url, targetSize);
Canvas.DrawHyperlink(Url, targetSize);
base.Draw(availableSpace);
}
}
+30 -43
View File
@@ -8,15 +8,7 @@ namespace QuestPDF.Elements
{
internal class InlinedElement : Container
{
public SpacePlan? MeasureCache { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
// TODO: once element caching proxy is introduces, this can be removed
MeasureCache ??= Child.Measure(Size.Max);
return MeasureCache.Value;
}
}
internal enum InlinedAlignment
@@ -112,8 +104,11 @@ namespace QuestPDF.Elements
foreach (var element in elements)
{
var size = element.Measure(Size.Max);
var size = (Size)element.Measure(Size.Max);
var baselineOffset = BaselineOffset(size, lineSize.Height);
if (size.Height == 0)
size = new Size(size.Width, lineSize.Height);
Canvas.Translate(new Position(0, baselineOffset));
element.Draw(size);
@@ -132,48 +127,39 @@ namespace QuestPDF.Elements
if (elements.Count == 1)
return 0;
if (ElementsAlignment == InlinedAlignment.Justify)
return difference / (elements.Count - 1);
if (ElementsAlignment == InlinedAlignment.SpaceAround)
return difference / (elements.Count + 1);
return HorizontalSpacing;
return ElementsAlignment switch
{
InlinedAlignment.Justify => difference / (elements.Count - 1),
InlinedAlignment.SpaceAround => difference / (elements.Count + 1),
_ => HorizontalSpacing
};
}
float AlignOffset()
{
if (ElementsAlignment == InlinedAlignment.Left)
return 0;
if (ElementsAlignment == InlinedAlignment.Justify)
return 0;
if (ElementsAlignment == InlinedAlignment.SpaceAround)
return elementOffset;
var difference = availableSpace.Width - lineSize.Width - (elements.Count - 1) * HorizontalSpacing;
if (ElementsAlignment == InlinedAlignment.Center)
return difference / 2;
if (ElementsAlignment == InlinedAlignment.Right)
return difference;
return 0;
return ElementsAlignment switch
{
InlinedAlignment.Left => 0,
InlinedAlignment.Justify => 0,
InlinedAlignment.SpaceAround => elementOffset,
InlinedAlignment.Center => difference / 2,
InlinedAlignment.Right => difference,
_ => 0
};
}
float BaselineOffset(Size elementSize, float lineHeight)
{
if (BaselineAlignment == VerticalAlignment.Top)
return 0;
var difference = lineHeight - elementSize.Height;
if (BaselineAlignment == VerticalAlignment.Middle)
return difference / 2;
return difference;
return BaselineAlignment switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Middle => difference / 2,
_ => difference
};
}
}
}
@@ -250,11 +236,12 @@ namespace QuestPDF.Elements
float GetInitialAlignmentOffset()
{
// this method makes sure that the spacing between elements is no lesser than configured
if (ElementsAlignment == InlinedAlignment.SpaceAround)
return HorizontalSpacing * 2;
return 0;
return ElementsAlignment switch
{
InlinedAlignment.SpaceAround => HorizontalSpacing * 2,
_ => 0
};
}
}
}
+46
View File
@@ -0,0 +1,46 @@
using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
public interface ILine
{
}
internal enum LineType
{
Vertical,
Horizontal
}
internal class Line : Element, ILine, ICacheable
{
public LineType Type { get; set; } = LineType.Vertical;
public string Color { get; set; } = Colors.Black;
public float Size { get; set; } = 1;
internal override SpacePlan Measure(Size availableSpace)
{
return Type switch
{
LineType.Vertical when availableSpace.Width >= Size => SpacePlan.FullRender(Size, 0),
LineType.Horizontal when availableSpace.Height >= Size => SpacePlan.FullRender(0, Size),
_ => SpacePlan.Wrap()
};
}
internal override void Draw(Size availableSpace)
{
if (Type == LineType.Vertical)
{
Canvas.DrawRectangle(new Position(-Size/2, 0), new Size(Size, availableSpace.Height), Color);
}
else if (Type == LineType.Horizontal)
{
Canvas.DrawRectangle(new Position(0, -Size/2), new Size(availableSpace.Width, Size), Color);
}
}
}
}
+2 -2
View File
@@ -45,7 +45,7 @@ namespace QuestPDF.Elements
.Decoration(decoration =>
{
decoration
.Header()
.Before()
.DebugPointer("Page header")
.Element(Header);
@@ -57,7 +57,7 @@ namespace QuestPDF.Elements
.Element(Content);
decoration
.Footer()
.After()
.DebugPointer("Page footer")
.Element(Footer);
});
+119 -127
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
@@ -6,171 +7,162 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class RowElement : Constrained
internal enum RowItemType
{
public float ConstantSize { get; }
public float RelativeSize { get; }
public RowElement(float constantSize, float relativeSize)
{
ConstantSize = constantSize;
RelativeSize = relativeSize;
}
public void SetWidth(float width)
{
MinWidth = width;
MaxWidth = width;
}
Auto,
Constant,
Relative
}
internal class BinaryRow : Element, ICacheable, IStateResettable
internal class RowItem : Container
{
internal Element Left { get; set; }
internal Element Right { get; set; }
private bool IsLeftRendered { get; set; }
private bool IsRightRendered { get; set; }
public bool IsRendered { get; set; }
public float Width { get; set; }
public RowItemType Type { get; set; }
public float Size { get; set; }
}
internal class RowItemRenderingCommand
{
public RowItem RowItem { get; set; }
public SpacePlan Measurement { get; set; }
public Size Size { get; set; }
public Position Offset { get; set; }
}
internal class Row : Element, ICacheable, IStateResettable
{
internal List<RowItem> Items { get; } = new();
internal float Spacing { get; set; }
public void ResetState()
{
IsLeftRendered = false;
IsRightRendered = false;
Items.ForEach(x => x.IsRendered = false);
}
internal override IEnumerable<Element?> GetChildren()
{
yield return Left;
yield return Right;
return Items;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Left = create(Left);
Right = create(Right);
Items.ForEach(x => x.Child = create(x.Child));
}
internal override SpacePlan Measure(Size availableSpace)
{
var leftMeasurement = Left.Measure(new Size(availableSpace.Width, availableSpace.Height));
if (!Items.Any())
return SpacePlan.FullRender(Size.Zero);
if (leftMeasurement.Type == SpacePlanType.Wrap)
UpdateItemsWidth(availableSpace.Width);
var renderingCommands = PlanLayout(availableSpace);
if (renderingCommands.Any(x => !x.RowItem.IsRendered && x.Measurement.Type == SpacePlanType.Wrap))
return SpacePlan.Wrap();
if (leftMeasurement.Type == SpacePlanType.FullRender)
IsLeftRendered = true;
var rightMeasurement = Right.Measure(new Size(availableSpace.Width - leftMeasurement.Width, availableSpace.Height));
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 (rightMeasurement.Type == SpacePlanType.Wrap)
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap();
if (leftMeasurement.Type == SpacePlanType.FullRender)
IsRightRendered = true;
var totalWidth = leftMeasurement.Width + rightMeasurement.Width;
var totalHeight = Math.Max(leftMeasurement.Height, rightMeasurement.Height);
if (renderingCommands.Any(x => !x.RowItem.IsRendered && x.Measurement.Type == SpacePlanType.PartialRender))
return SpacePlan.PartialRender(size);
var targetSize = new Size(totalWidth, totalHeight);
if ((!IsLeftRendered && leftMeasurement.Type == SpacePlanType.PartialRender) ||
(!IsRightRendered && rightMeasurement.Type == SpacePlanType.PartialRender))
return SpacePlan.PartialRender(targetSize);
return SpacePlan.FullRender(targetSize);
return SpacePlan.FullRender(size);
}
internal override void Draw(Size availableSpace)
{
var leftMeasurement = Left.Measure(new Size(availableSpace.Width, availableSpace.Height));
var leftWidth = leftMeasurement.Width;
Left.Draw(new Size(leftWidth, availableSpace.Height));
Canvas.Translate(new Position(leftWidth, 0));
Right.Draw(new Size(availableSpace.Width - leftWidth, availableSpace.Height));
Canvas.Translate(new Position(-leftWidth, 0));
}
}
internal class Row : Element
{
public float Spacing { get; set; } = 0;
public ICollection<RowElement> Items { get; internal set; } = new List<RowElement>();
private Element? RootElement { get; set; }
if (!Items.Any())
return;
internal override IEnumerable<Element?> GetChildren()
{
if (RootElement == null)
ComposeTree();
UpdateItemsWidth(availableSpace.Width);
var renderingCommands = PlanLayout(availableSpace);
yield return RootElement;
}
internal override SpacePlan Measure(Size availableSpace)
{
UpdateElementsWidth(availableSpace.Width);
return RootElement.Measure(availableSpace);
}
internal override void Draw(Size availableSpace)
{
UpdateElementsWidth(availableSpace.Width);
RootElement.Draw(availableSpace);
}
#region structure
private void ComposeTree()
{
Items = AddSpacing(Items, Spacing);
var elements = Items.Cast<Element>().ToArray();
RootElement = BuildTree(elements);
}
private void UpdateElementsWidth(float availableWidth)
{
var constantWidth = Items.Sum(x => x.ConstantSize);
var relativeWidth = Items.Sum(x => x.RelativeSize);
var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
foreach (var row in Items)
foreach (var command in renderingCommands)
{
row.SetWidth(row.ConstantSize + row.RelativeSize * widthPerRelativeUnit);
if (command.Measurement.Type == SpacePlanType.FullRender)
command.RowItem.IsRendered = true;
if (command.Measurement.Type == SpacePlanType.Wrap)
continue;
Canvas.Translate(command.Offset);
command.RowItem.Draw(command.Size);
Canvas.Translate(command.Offset.Reverse());
}
if (Items.All(x => x.IsRendered))
ResetState();
}
private void UpdateItemsWidth(float availableWidth)
{
HandleItemsWithAutoWidth();
var constantWidth = Items.Where(x => x.Type == RowItemType.Constant).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.Constant))
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 void HandleItemsWithAutoWidth()
{
foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto))
{
rowItem.Size = rowItem.Measure(Size.Max).Width;
rowItem.Type = RowItemType.Constant;
}
}
private static ICollection<RowElement> AddSpacing(ICollection<RowElement> elements, float spacing)
private ICollection<RowItemRenderingCommand> PlanLayout(Size availableSpace)
{
if (spacing < Size.Epsilon)
return elements;
return elements
.SelectMany(x => new[] { new RowElement(spacing, 0), x })
.Skip(1)
.ToList();
}
var leftOffset = 0f;
var renderingCommands = new List<RowItemRenderingCommand>();
private static Element BuildTree(Span<Element> elements)
{
if (elements.IsEmpty)
return Empty.Instance;
if (elements.Length == 1)
return elements[0];
var half = elements.Length / 2;
return new BinaryRow
foreach (var item in Items)
{
Left = BuildTree(elements.Slice(0, half)),
Right = BuildTree(elements.Slice(half))
};
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;
}
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;
}
#endregion
}
}
+78
View File
@@ -0,0 +1,78 @@
using System;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ScaleToFit : ContainerElement
{
internal override SpacePlan Measure(Size availableSpace)
{
if (Child == null)
return SpacePlan.FullRender(Size.Zero);
var perfectScale = FindPerfectScale(Child, availableSpace);
if (perfectScale == null)
return SpacePlan.Wrap();
var targetSpace = ScaleSize(availableSpace, perfectScale.Value);
return SpacePlan.FullRender(targetSpace);
}
internal override void Draw(Size availableSpace)
{
var perfectScale = FindPerfectScale(Child, 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 static float? FindPerfectScale(Element child, 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 child.Measure(scaledSpace).Type == SpacePlanType.FullRender;
}
}
}
}
@@ -2,7 +2,7 @@
namespace QuestPDF.Elements
{
internal class InternalLocation : ContainerElement, IStateResettable
internal class Section : ContainerElement, IStateResettable
{
public string LocationName { get; set; }
private bool IsRendered { get; set; }
@@ -16,12 +16,11 @@ namespace QuestPDF.Elements
{
if (!IsRendered)
{
PageContext.SetLocationPage(LocationName);
Canvas.DrawLocation(LocationName);
Canvas.DrawSection(LocationName);
IsRendered = true;
}
PageContext.SetSectionPage(LocationName);
base.Draw(availableSpace);
}
}
@@ -3,9 +3,9 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class InternalLink : ContainerElement
internal class SectionLink : ContainerElement
{
public string LocationName { get; set; }
public string SectionName { get; set; }
internal override void Draw(Size availableSpace)
{
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
if (targetSize.Type == SpacePlanType.Wrap)
return;
Canvas.DrawLocationLink(LocationName, targetSize);
Canvas.DrawSectionLink(SectionName, targetSize);
base.Draw(availableSpace);
}
}
-143
View File
@@ -1,143 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using IComponent = QuestPDF.Infrastructure.IComponent;
using IContainer = QuestPDF.Infrastructure.IContainer;
namespace QuestPDF.Elements
{
internal class BinaryStack : Element, IStateResettable, ICacheable
{
internal Element First { get; set; } = Empty.Instance;
internal Element Second { get; set; } = Empty.Instance;
internal bool IsFirstRendered { get; set; } = false;
internal override IEnumerable<Element?> GetChildren()
{
yield return First;
yield return Second;
}
public void ResetState()
{
IsFirstRendered = false;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
First = create(First);
Second = create(Second);
}
internal override SpacePlan Measure(Size availableSpace)
{
var firstElement = IsFirstRendered ? Empty.Instance : First;
var firstSize = firstElement.Measure(availableSpace);
if (firstSize.Type == SpacePlanType.Wrap)
return SpacePlan.Wrap();
if (firstSize.Type == SpacePlanType.PartialRender)
return firstSize;
var spaceForSecond = new Size(availableSpace.Width, availableSpace.Height - firstSize.Height);
var secondSize = Second.Measure(spaceForSecond);
if (secondSize.Type == SpacePlanType.Wrap)
return SpacePlan.PartialRender(firstSize);
var totalWidth = Math.Max(firstSize.Width, secondSize.Width);
var totalHeight = firstSize.Height + secondSize.Height;
var targetSize = new Size(totalWidth, totalHeight);
if (secondSize.Type == SpacePlanType.PartialRender)
return SpacePlan.PartialRender(targetSize);
return SpacePlan.FullRender(targetSize);
}
internal override void Draw(Size availableSpace)
{
var firstElement = IsFirstRendered ? Empty.Instance : First;
var firstMeasurement = firstElement.Measure(availableSpace);
if (firstMeasurement.Type == SpacePlanType.FullRender)
IsFirstRendered = true;
var firstSize = firstMeasurement;
if (firstSize.Type != SpacePlanType.Wrap)
firstElement.Draw(new Size(availableSpace.Width, firstSize.Height));
if (firstMeasurement.Type == SpacePlanType.Wrap || firstMeasurement.Type == SpacePlanType.PartialRender)
return;
var firstHeight = firstSize.Height;
var spaceForSecond = new Size(availableSpace.Width, availableSpace.Height - firstHeight);
var secondMeasurement = Second.Measure(spaceForSecond);
if (secondMeasurement.Type == SpacePlanType.Wrap)
return;
Canvas.Translate(new Position(0, firstHeight));
Second.Draw(new Size(availableSpace.Width, secondMeasurement.Height));
Canvas.Translate(new Position(0, -firstHeight));
if (secondMeasurement.Type == SpacePlanType.FullRender)
IsFirstRendered = false;
}
}
internal class Stack : IComponent
{
public ICollection<Element> Items { get; } = new List<Element>();
public float Spacing { get; set; } = 0;
public void Compose(IContainer container)
{
var elements = AddSpacing(Spacing, Items);
container
.PaddingBottom(-Spacing)
.Element(BuildTree(elements.ToArray()));
}
static ICollection<Element> AddSpacing(float spacing, ICollection<Element> elements)
{
if (spacing < Size.Epsilon)
return elements;
return elements
.Where(x => !(x is Empty))
.Select(x => new Padding
{
Bottom = spacing,
Child = x
})
.Cast<Element>()
.ToList();
}
static Element BuildTree(Span<Element> elements)
{
if (elements.IsEmpty)
return Empty.Instance;
if (elements.Length == 1)
return elements[0];
var half = elements.Length / 2;
return new BinaryStack
{
First = BuildTree(elements.Slice(0, half)),
Second = BuildTree(elements.Slice(half))
};
}
}
}
+25
View File
@@ -0,0 +1,25 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class StopPaging : ContainerElement
{
internal override SpacePlan Measure(Size availableSpace)
{
if (Child == null)
return SpacePlan.FullRender(Size.Zero);
var measurement = Child.Measure(availableSpace);
return measurement.Type switch
{
SpacePlanType.Wrap => SpacePlan.FullRender(Size.Zero),
SpacePlanType.PartialRender => SpacePlan.FullRender(measurement),
SpacePlanType.FullRender => measurement,
_ => throw new ArgumentOutOfRangeException()
};
}
}
}
+17 -35
View File
@@ -15,15 +15,19 @@ namespace QuestPDF.Elements.Table
public List<TableCell> Cells { get; set; } = new List<TableCell>();
public bool ExtendLastCellsToTableBottom { get; set; }
// cache for efficient cell finding
// index of first array - number of row
// nested array - collection of all cells starting at given row
private TableCell[][] OrderedCells { get; set; }
private int StartingRowsCount { get; set; }
private int RowsCount { get; set; }
private int CurrentRow { get; set; }
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
{
StartingRowsCount = Cells.Select(x => x.Row).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();
base.Initialize(pageContext, canvas);
}
internal override IEnumerable<Element?> GetChildren()
{
return Cells;
@@ -31,24 +35,6 @@ namespace QuestPDF.Elements.Table
public void ResetState()
{
if (StartingRowsCount == default)
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
if (RowsCount == default)
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
if (OrderedCells == default)
{
var groups = Cells
.GroupBy(x => x.Row)
.ToDictionary(x => x.Key, x => x.OrderBy(y => y.Column).ToArray());
OrderedCells = Enumerable
.Range(0, RowsCount + 1)
.Select(x => groups.TryGetValue(x, out var output) ? output : Array.Empty<TableCell>())
.ToArray();
}
Cells.ForEach(x => x.IsRendered = false);
CurrentRow = 1;
}
@@ -85,6 +71,9 @@ namespace QuestPDF.Elements.Table
{
if (command.Measurement.Type == SpacePlanType.FullRender)
command.Cell.IsRendered = true;
if (command.Measurement.Type == SpacePlanType.Wrap)
continue;
Canvas.Translate(command.Offset);
command.Cell.Draw(command.Size);
@@ -103,7 +92,7 @@ namespace QuestPDF.Elements.Table
{
return commands
.GroupBy(x => x.Cell.Row)
.Where(x => x.All(y => y.Measurement.Type == SpacePlanType.FullRender))
.Where(x => x.All(y => y.Cell.IsRendered || y.Measurement.Type == SpacePlanType.FullRender))
.Select(x => x.Key)
.DefaultIfEmpty(0)
.Max();
@@ -152,16 +141,12 @@ namespace QuestPDF.Elements.Table
ICollection<TableCellRenderingCommand> GetRenderingCommands()
{
var rowBottomOffsets = new DynamicDictionary<int, float>();
var commands = new List<TableCellRenderingCommand>();
var cellsToTry = Enumerable
.Range(CurrentRow, RowsCount - CurrentRow + 1)
.SelectMany(x => OrderedCells[x]);
var cellsToTry = Cells.Where(x => x.Row + x.RowSpan - 1 >= CurrentRow);
var currentRow = CurrentRow;
var maxRenderingRow = RowsCount;
var commands = new List<TableCellRenderingCommand>();
foreach (var cell in cellsToTry)
{
// update position of previous row
@@ -181,10 +166,7 @@ namespace QuestPDF.Elements.Table
// cell visibility optimizations
if (cell.Row > maxRenderingRow)
break;
if (cell.IsRendered)
continue;
// calculate cell position / size
var topOffset = rowBottomOffsets[cell.Row - 1];
@@ -274,4 +256,4 @@ namespace QuestPDF.Elements.Table
}
}
}
}
}
@@ -2,7 +2,7 @@
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextMeasurementResult
internal class TextBlockSize
{
public float Width { get; set; }
public float Height => Math.Abs(Descent) + Math.Abs(Ascent);
@@ -11,12 +11,5 @@ namespace QuestPDF.Elements.Text.Calculation
public float Descent { get; set; }
public float LineHeight { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public int NextIndex { get; set; }
public int TotalIndex { get; set; }
public bool IsLast => EndIndex == TotalIndex;
}
}
@@ -2,14 +2,8 @@
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextDrawingRequest
internal struct TextDrawingRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public float TotalAscent { get; set; }
public Size TextSize { get; set; }
}
@@ -5,6 +5,6 @@ namespace QuestPDF.Elements.Text.Calculation
internal class TextLineElement
{
public ITextBlockItem Item { get; set; }
public TextMeasurementResult Measurement { get; set; }
public TextBlockSize Measurement { get; set; }
}
}
@@ -1,14 +0,0 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextMeasurementRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public float AvailableWidth { get; set; }
public bool IsFirstLineElement { get; set; }
}
}
@@ -5,7 +5,10 @@ namespace QuestPDF.Elements.Text.Items
{
internal interface ITextBlockItem
{
TextMeasurementResult? Measure(TextMeasurementRequest request);
ICanvas Canvas { get; set; }
IPageContext PageContext { get; set; }
TextBlockSize? Measure();
void Draw(TextDrawingRequest request);
}
}
@@ -7,41 +7,40 @@ namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockElement : ITextBlockItem
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public Element Element { get; set; } = Empty.Instance;
public TextMeasurementResult? Measure(TextMeasurementRequest request)
public TextBlockSize? Measure()
{
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
var measurement = Element.Measure(Size.Max);
if (measurement.Type != SpacePlanType.FullRender)
return null;
return new TextMeasurementResult
return new TextBlockSize
{
Width = measurement.Width,
Ascent = -measurement.Height,
Descent = 0,
LineHeight = 1,
StartIndex = 0,
EndIndex = 0,
TotalIndex = 0
LineHeight = 1
};
}
public void Draw(TextDrawingRequest request)
{
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
request.Canvas.Translate(new Position(0, request.TotalAscent));
Canvas.Translate(new Position(0, request.TotalAscent));
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
Canvas.Translate(new Position(0, -request.TotalAscent));
}
}
}
@@ -1,24 +0,0 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockExternalLink : TextBlockSpan
{
public string Url { get; set; }
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return MeasureWithoutCache(request);
}
public override void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawExternalLink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
@@ -0,0 +1,19 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockHyperlink : TextBlockSpan
{
public string Url { get; set; }
public override void Draw(TextDrawingRequest request)
{
Canvas.Translate(new Position(0, request.TotalAscent));
Canvas.DrawHyperlink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
@@ -1,24 +0,0 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockInternalLink : TextBlockSpan
{
public string LocationName { get; set; }
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return MeasureWithoutCache(request);
}
public override void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawLocationLink(LocationName, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
@@ -1,33 +1,29 @@
using QuestPDF.Elements.Text.Calculation;
using System;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockPageNumber : TextBlockSpan
{
public string SlotName { get; set; }
public const string PageNumberPlaceholder = "123";
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
public override TextBlockSize? Measure()
{
SetPageNumber(request.PageContext);
return MeasureWithoutCache(request);
SetPageNumber();
return base.Measure();
}
public override void Draw(TextDrawingRequest request)
{
SetPageNumber(request.PageContext);
SetPageNumber();
base.Draw(request);
}
private void SetPageNumber(IPageContext context)
private void SetPageNumber()
{
var pageNumberPlaceholder = 123;
var pageNumber = context.GetRegisteredLocations().Contains(SlotName)
? context.GetLocationPage(SlotName)
: pageNumberPlaceholder;
Text = pageNumber.ToString();
Text = Source(PageContext) ?? PageNumberPlaceholder;
}
}
}
@@ -0,0 +1,19 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSectionLink : TextBlockSpan
{
public string SectionName { get; set; }
public override void Draw(TextDrawingRequest request)
{
Canvas.Translate(new Position(0, request.TotalAscent));
Canvas.DrawSectionLink(SectionName, new Size(request.TextSize.Width, request.TextSize.Height));
Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
+56 -86
View File
@@ -1,118 +1,88 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
using SkiaSharp.HarfBuzz;
using Size = QuestPDF.Infrastructure.Size;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSpan : ITextBlockItem
{
public string Text { get; set; }
public TextStyle Style { get; set; } = new TextStyle();
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache =
new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>();
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
{
var cacheKey = (request.StartIndex, request.AvailableWidth);
if (!MeasureCache.ContainsKey(cacheKey))
MeasureCache[cacheKey] = MeasureWithoutCache(request);
return MeasureCache[cacheKey];
}
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
public string Text { get; set; }
public TextStyle Style { get; set; }
protected TextBlockSize? Size { get; set; }
protected bool RequiresShaping { get; set; }
public virtual TextBlockSize? Measure()
{
Size ??= Text == " " ? GetSizeForSpace() : GetSizeForWord();
return Size;
}
private TextBlockSize GetSizeForWord()
{
const char space = ' ';
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
var startIndex = request.StartIndex;
var shaper = Style.ToShaper();
if (request.IsFirstLineElement)
{
while (startIndex + 1 < Text.Length && Text[startIndex] == space)
startIndex++;
}
if (Text.Length == 0)
{
return new TextMeasurementResult
{
Width = 0,
LineHeight = Style.LineHeight ?? 1,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent
};
}
// shaper returns positions of all glyphs,
// by adding a space, it is possible to capture width of the last original character
var result = shaper.Shape(Text + " ", paint);
// start breaking text from requested position
var text = Text.Substring(startIndex);
// when text is left-to-right: last value corresponds to text width
// when text is right-to-left: glyphs are in the reverse order, first value represents text width
var width = Math.Max(result.Points.First().X, result.Points.Last().X);
RequiresShaping = result.Points.Length != Text.Length + 1;
var textLength = (int)paint.BreakText(text, request.AvailableWidth);
if (textLength <= 0)
return null;
if (textLength < text.Length && text[textLength] == space)
textLength++;
// break text only on spaces
if (textLength < text.Length)
{
var lastSpaceIndex = text.Substring(0, textLength).LastIndexOf(space) - 1;
if (lastSpaceIndex <= 0)
{
if (!request.IsFirstLineElement)
return null;
}
else
{
textLength = lastSpaceIndex + 1;
}
}
text = text.Substring(0, textLength);
var endIndex = startIndex + textLength;
var nextIndex = endIndex;
while (nextIndex + 1 < Text.Length && Text[nextIndex] == space)
nextIndex++;
// measure final text
var width = paint.MeasureText(text);
return new TextMeasurementResult
return new TextBlockSize
{
Width = width,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight ?? 1,
StartIndex = startIndex,
EndIndex = endIndex,
NextIndex = nextIndex,
TotalIndex = Text.Length
LineHeight = Style.LineHeight ?? 1
};
}
private TextBlockSize GetSizeForSpace()
{
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
return new TextBlockSize
{
Width = paint.MeasureText(" "),
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight ?? 1
};
}
public virtual void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
request.Canvas.DrawText(text, Position.Zero, Style);
Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
if (!string.IsNullOrWhiteSpace(Text))
{
if (RequiresShaping)
Canvas.DrawShapedText(Text, Position.Zero, Style);
else
Canvas.DrawText(Text, Position.Zero, Style);
}
// draw underline
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
@@ -124,7 +94,7 @@ namespace QuestPDF.Elements.Text.Items
void DrawLine(float offset, float thickness)
{
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
}
}
}
+98 -75
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Elements.Text.Items;
@@ -11,108 +12,145 @@ namespace QuestPDF.Elements.Text
internal class TextBlock : Element, IStateResettable
{
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
public List<ITextBlockItem> Items { get; set; } = new();
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
private Queue<ITextBlockItem> RenderingQueue { get; set; }
private int CurrentElementIndex { get; set; }
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
{
Items = SplitElementsBySpace().ToList();
Items.ForEach(x =>
{
x.PageContext = pageContext;
x.Canvas = canvas;
});
base.Initialize(pageContext, canvas);
}
public void ResetState()
{
RenderingQueue = new Queue<ITextBlockItem>(Items);
CurrentElementIndex = 0;
}
private IEnumerable<ITextBlockItem> SplitElementsBySpace()
{
foreach (var item in Items)
{
if (item is TextBlockSpan span)
{
span.Text ??= "";
foreach (var spanItem in Regex.Split(span.Text, "( )|([^ ]+)"))
{
yield return new TextBlockSpan
{
Text = spanItem,
Style = span.Style
};
}
}
}
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!RenderingQueue.Any())
if (!Items.Any())
return SpacePlan.FullRender(Size.Zero);
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
if (!lines.Any())
return SpacePlan.Wrap();
var width = lines.Max(x => x.Width);
var height = lines.Sum(x => x.LineHeight);
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap();
var fullyRenderedItemsCount = lines
.SelectMany(x => x.Elements)
.GroupBy(x => x.Item)
.Count(x => x.Any(y => y.Measurement.IsLast));
if (fullyRenderedItemsCount == RenderingQueue.Count)
if (CurrentElementIndex + lines.Sum(x => x.Elements.Count) == Items.Count)
return SpacePlan.FullRender(width, height);
return SpacePlan.PartialRender(width, height);
}
internal override void Draw(Size availableSpace)
{
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
if (!lines.Any())
return;
var heightOffset = 0f;
var widthOffset = 0f;
foreach (var line in lines)
var isLastPart = lines.Sum(x => x.Elements.Count) + CurrentElementIndex == Items.Count;
foreach (var sourceLine in lines)
{
widthOffset = 0f;
var alignmentOffset = GetAlignmentOffset(line.Width);
var isLastLine = sourceLine == lines.Last();
var line = sourceLine;
if (!(isLastPart && isLastLine))
{
var spans = sourceLine.Elements
.Where(x => x.Item is TextBlockSpan)
.SkipWhile(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Reverse()
.SkipWhile(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Reverse()
.ToList();
var wordsWidth = spans
.Where(x => !string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Sum(x => x.Measurement.Width);
var spaceSpans = spans.Where(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text)).ToList();
var spaceWidth = (availableSpace.Width - wordsWidth) / spaceSpans.Count;
spaceSpans.ForEach(x => x.Measurement.Width = spaceWidth);
line = TextLine.From(spans);
}
var alignmentOffset = GetAlignmentOffset(line.Width);
Canvas.Translate(new Position(alignmentOffset, 0));
Canvas.Translate(new Position(0, -line.Ascent));
foreach (var item in line.Elements)
{
var textDrawingRequest = new TextDrawingRequest
{
Canvas = Canvas,
PageContext = PageContext,
StartIndex = item.Measurement.StartIndex,
EndIndex = item.Measurement.EndIndex,
TextSize = new Size(item.Measurement.Width, line.LineHeight),
TotalAscent = line.Ascent
};
item.Item.Draw(textDrawingRequest);
Canvas.Translate(new Position(item.Measurement.Width, 0));
widthOffset += item.Measurement.Width;
}
Canvas.Translate(new Position(-alignmentOffset, 0));
Canvas.Translate(new Position(-line.Width, line.Ascent));
Canvas.Translate(new Position(0, line.LineHeight));
heightOffset += line.LineHeight;
}
Canvas.Translate(new Position(0, -heightOffset));
lines
.SelectMany(x => x.Elements)
.GroupBy(x => x.Item)
.Where(x => x.Any(y => y.Measurement.IsLast))
.Select(x => x.Key)
.ToList()
.ForEach(x => RenderingQueue.Dequeue());
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
Canvas.Translate(new Position(0, -heightOffset));
CurrentElementIndex += lines.Sum(x => x.Elements.Count);
if (!RenderingQueue.Any())
if (CurrentElementIndex == Items.Count)
ResetState();
float GetAlignmentOffset(float lineWidth)
{
if (Alignment == HorizontalAlignment.Left)
@@ -130,19 +168,18 @@ namespace QuestPDF.Elements.Text
}
}
public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
private IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
{
var queue = new Queue<ITextBlockItem>(RenderingQueue);
var currentItemIndex = CurrentElementIndex;
var currentHeight = 0f;
while (queue.Any())
while (true)
{
var line = GetNextLine();
if (!line.Elements.Any())
yield break;
if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
yield break;
@@ -155,43 +192,29 @@ namespace QuestPDF.Elements.Text
var currentWidth = 0f;
var currentLineElements = new List<TextLineElement>();
while (true)
{
if (!queue.Any())
if (currentItemIndex == Items.Count)
break;
var currentElement = queue.Peek();
var measurementRequest = new TextMeasurementRequest
{
Canvas = Canvas,
PageContext = PageContext,
StartIndex = currentItemIndex,
AvailableWidth = availableWidth - currentWidth,
IsFirstLineElement = !currentLineElements.Any()
};
var measurementResponse = currentElement.Measure(measurementRequest);
if (measurementResponse == null)
var currentElement = Items[currentItemIndex];
var textBlockSize = currentElement.Measure();
if (textBlockSize == null)
break;
if (currentWidth + textBlockSize.Width > availableWidth + Size.Epsilon)
break;
currentLineElements.Add(new TextLineElement
{
Item = currentElement,
Measurement = measurementResponse
Measurement = textBlockSize
});
currentWidth += measurementResponse.Width;
currentItemIndex = measurementResponse.NextIndex;
if (!measurementResponse.IsLast)
break;
currentItemIndex = 0;
queue.Dequeue();
currentWidth += textBlockSize.Width;
currentItemIndex ++;
}
return TextLine.From(currentLineElements);
+20 -14
View File
@@ -14,39 +14,45 @@ namespace QuestPDF.Fluent
return element.Element(border);
}
public static IContainer Border(this IContainer element, float value)
public static IContainer Border(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.BorderHorizontal(value).BorderVertical(value);
return element
.BorderHorizontal(value, unit)
.BorderVertical(value, unit);
}
public static IContainer BorderVertical(this IContainer element, float value)
public static IContainer BorderVertical(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.BorderLeft(value).BorderRight(value);
return element
.BorderLeft(value, unit)
.BorderRight(value, unit);
}
public static IContainer BorderHorizontal(this IContainer element, float value)
public static IContainer BorderHorizontal(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.BorderTop(value).BorderBottom(value);
return element
.BorderTop(value, unit)
.BorderBottom(value, unit);
}
public static IContainer BorderLeft(this IContainer element, float value)
public static IContainer BorderLeft(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Border(x => x.Left = value);
return element.Border(x => x.Left = value.ToPoints(unit));
}
public static IContainer BorderRight(this IContainer element, float value)
public static IContainer BorderRight(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Border(x => x.Right = value);
return element.Border(x => x.Right = value.ToPoints(unit));
}
public static IContainer BorderTop(this IContainer element, float value)
public static IContainer BorderTop(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Border(x => x.Top = value);
return element.Border(x => x.Top = value.ToPoints(unit));
}
public static IContainer BorderBottom(this IContainer element, float value)
public static IContainer BorderBottom(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Border(x => x.Bottom = value);
return element.Border(x => x.Bottom = value.ToPoints(unit));
}
public static IContainer BorderColor(this IContainer element, string color)
+44
View File
@@ -0,0 +1,44 @@
using System;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class ColumnDescriptor
{
internal Column Column { get; } = new();
public void Spacing(float value, Unit unit = Unit.Point)
{
Column.Spacing = value.ToPoints(unit);
}
public IContainer Item()
{
var container = new Container();
Column.Items.Add(new ColumnItem
{
Child = container
});
return container;
}
}
public static class ColumnExtensions
{
[Obsolete("This element has been renamed since version 2022.2. Please use the 'Column' method.")]
public static void Stack(this IContainer element, Action<ColumnDescriptor> handler)
{
element.Column(handler);
}
public static void Column(this IContainer element, Action<ColumnDescriptor> handler)
{
var descriptor = new ColumnDescriptor();
handler(descriptor);
element.Element(descriptor.Column);
}
}
}
+16 -12
View File
@@ -14,34 +14,38 @@ namespace QuestPDF.Fluent
return element.Element(constrained);
}
public static IContainer Width(this IContainer element, float value)
public static IContainer Width(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.MinWidth(value).MaxWidth(value);
return element
.MinWidth(value, unit)
.MaxWidth(value, unit);
}
public static IContainer MinWidth(this IContainer element, float value)
public static IContainer MinWidth(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Constrained(x => x.MinWidth = value);
return element.Constrained(x => x.MinWidth = value.ToPoints(unit));
}
public static IContainer MaxWidth(this IContainer element, float value)
public static IContainer MaxWidth(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Constrained(x => x.MaxWidth = value);
return element.Constrained(x => x.MaxWidth = value.ToPoints(unit));
}
public static IContainer Height(this IContainer element, float value)
public static IContainer Height(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.MinHeight(value).MaxHeight(value);
return element
.MinHeight(value, unit)
.MaxHeight(value, unit);
}
public static IContainer MinHeight(this IContainer element, float value)
public static IContainer MinHeight(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Constrained(x => x.MinHeight = value);
return element.Constrained(x => x.MinHeight = value.ToPoints(unit));
}
public static IContainer MaxHeight(this IContainer element, float value)
public static IContainer MaxHeight(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Constrained(x => x.MaxHeight = value);
return element.Constrained(x => x.MaxHeight = value.ToPoints(unit));
}
}
}
+39 -7
View File
@@ -8,16 +8,16 @@ namespace QuestPDF.Fluent
{
internal Decoration Decoration { get; } = new Decoration();
public IContainer Header()
public IContainer Before()
{
var container = new Container();
Decoration.Header = container;
Decoration.Before = container;
return container;
}
public void Header(Action<IContainer> handler)
public void Before(Action<IContainer> handler)
{
handler?.Invoke(Header());
handler?.Invoke(Before());
}
public IContainer Content()
@@ -32,17 +32,49 @@ namespace QuestPDF.Fluent
handler?.Invoke(Content());
}
public IContainer Footer()
public IContainer After()
{
var container = new Container();
Decoration.Footer = container;
Decoration.After = container;
return container;
}
public void After(Action<IContainer> handler)
{
handler?.Invoke(After());
}
#region Obsolete
[Obsolete("This element has been renamed since version 2022.2. Please use the 'Before' method.")]
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.")]
public void Header(Action<IContainer> handler)
{
handler?.Invoke(Header());
}
[Obsolete("This element has been renamed since version 2022.2. Please use the 'After' method.")]
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.")]
public void Footer(Action<IContainer> handler)
{
handler?.Invoke(Footer());
}
#endregion
}
public static class DecorationExtensions
@@ -52,7 +84,7 @@ namespace QuestPDF.Fluent
var descriptor = new DecorationDescriptor();
handler(descriptor);
element.Component(descriptor.Decoration);
element.Element(descriptor.Decoration);
}
}
}
+35 -8
View File
@@ -60,7 +60,7 @@ namespace QuestPDF.Fluent
public static void Placeholder(this IContainer element, string? text = null)
{
element.Component(new Elements.Placeholder
element.Component(new Placeholder
{
Text = text ?? string.Empty
});
@@ -99,27 +99,45 @@ namespace QuestPDF.Fluent
return element.Element(new Container());
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
public static IContainer ExternalLink(this IContainer element, string url)
{
return element.Element(new ExternalLink
return element.Hyperlink(url);
}
public static IContainer Hyperlink(this IContainer element, string url)
{
return element.Element(new Hyperlink
{
Url = url
});
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Section method.")]
public static IContainer Location(this IContainer element, string locationName)
{
return element.Element(new InternalLocation
return element.Section(locationName);
}
public static IContainer Section(this IContainer element, string sectionName)
{
return element.Element(new Section
{
LocationName = locationName
LocationName = sectionName
});
}
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
public static IContainer InternalLink(this IContainer element, string locationName)
{
return element.Element(new InternalLink
return element.SectionLink(locationName);
}
public static IContainer SectionLink(this IContainer element, string sectionName)
{
return element.Element(new SectionLink
{
LocationName = locationName
SectionName = sectionName
});
}
@@ -136,8 +154,7 @@ namespace QuestPDF.Fluent
});
}
// TODO: deprecated Box method in QuestPDF 2022.1
[Obsolete("This element has been renamed. Please use the MinimalBox method.")]
[Obsolete("This element has been renamed since version 2022.1. Please use the MinimalBox method.")]
public static IContainer Box(this IContainer element)
{
return element.Element(new MinimalBox());
@@ -160,5 +177,15 @@ namespace QuestPDF.Fluent
TextStyle = textStyle
});
}
public static IContainer StopPaging(this IContainer element)
{
return element.Element(new StopPaging());
}
public static IContainer ScaleToFit(this IContainer element)
{
return element.Element(new ScaleToFit());
}
}
}
+7 -7
View File
@@ -8,20 +8,20 @@ namespace QuestPDF.Fluent
{
internal Grid Grid { get; } = new Grid();
public void Spacing(float value)
public void Spacing(float value, Unit unit = Unit.Point)
{
VerticalSpacing(value);
HorizontalSpacing(value);
VerticalSpacing(value, unit);
HorizontalSpacing(value, unit);
}
public void VerticalSpacing(float value)
public void VerticalSpacing(float value, Unit unit = Unit.Point)
{
Grid.VerticalSpacing = value;
Grid.VerticalSpacing = value.ToPoints(unit);
}
public void HorizontalSpacing(float value)
public void HorizontalSpacing(float value, Unit unit = Unit.Point)
{
Grid.HorizontalSpacing = value;
Grid.HorizontalSpacing = value.ToPoints(unit);
}
public void Columns(int value = Grid.DefaultColumnsCount)
+12 -5
View File
@@ -10,14 +10,21 @@ namespace QuestPDF.Fluent
{
internal Inlined Inlined { get; } = new Inlined();
public void Spacing(float value)
public void Spacing(float value, Unit unit = Unit.Point)
{
VerticalSpacing(value);
HorizontalSpacing(value);
VerticalSpacing(value, unit);
HorizontalSpacing(value, unit);
}
public void VerticalSpacing(float value) => Inlined.VerticalSpacing = value;
public void HorizontalSpacing(float value) => Inlined.HorizontalSpacing = value;
public void VerticalSpacing(float value, Unit unit = Unit.Point)
{
Inlined.VerticalSpacing = value.ToPoints(unit);
}
public void HorizontalSpacing(float value, Unit unit = Unit.Point)
{
Inlined.HorizontalSpacing = value.ToPoints(unit);
}
public void BaselineTop() => Inlined.BaselineAlignment = VerticalAlignment.Top;
public void BaselineMiddle() => Inlined.BaselineAlignment = VerticalAlignment.Middle;
+36
View File
@@ -0,0 +1,36 @@
using System;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class LineExtensions
{
private static ILine Line(this IContainer element, LineType type, float size)
{
var line = new Line
{
Size = size,
Type = type
};
element.Element(line);
return line;
}
public static ILine LineVertical(this IContainer element, float size, Unit unit = Unit.Point)
{
return element.Line(LineType.Vertical, size.ToPoints(unit));
}
public static ILine LineHorizontal(this IContainer element, float size, Unit unit = Unit.Point)
{
return element.Line(LineType.Horizontal, size.ToPoints(unit));
}
public static void LineColor(this ILine descriptor, string value)
{
(descriptor as Line).Color = value;
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class Document : IDocument
{
private Action<IDocumentContainer> ContentSource { get; }
private DocumentMetadata Metadata { get; set; } = DocumentMetadata.Default;
private Document(Action<IDocumentContainer> contentSource)
{
ContentSource = contentSource;
}
public static Document Create(Action<IDocumentContainer> handler)
{
return new Document(handler);
}
public Document WithMetadata(DocumentMetadata metadata)
{
Metadata = metadata ?? Metadata;
return this;
}
#region IDocument
public DocumentMetadata GetMetadata() => Metadata;
public void Compose(IDocumentContainer container) => ContentSource(container);
#endregion
}
}
+20 -14
View File
@@ -14,39 +14,45 @@ namespace QuestPDF.Fluent
return element.Element(padding);
}
public static IContainer Padding(this IContainer element, float value)
public static IContainer Padding(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.PaddingVertical(value).PaddingHorizontal(value);
return element
.PaddingVertical(value, unit)
.PaddingHorizontal(value, unit);
}
public static IContainer PaddingHorizontal(this IContainer element, float value)
public static IContainer PaddingHorizontal(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.PaddingLeft(value).PaddingRight(value);
return element
.PaddingLeft(value, unit)
.PaddingRight(value, unit);
}
public static IContainer PaddingVertical(this IContainer element, float value)
public static IContainer PaddingVertical(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.PaddingTop(value).PaddingBottom(value);
return element
.PaddingTop(value, unit)
.PaddingBottom(value, unit);
}
public static IContainer PaddingTop(this IContainer element, float value)
public static IContainer PaddingTop(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Padding(x => x.Top = value);
return element.Padding(x => x.Top += value.ToPoints(unit));
}
public static IContainer PaddingBottom(this IContainer element, float value)
public static IContainer PaddingBottom(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Padding(x => x.Bottom = value);
return element.Padding(x => x.Bottom += value.ToPoints(unit));
}
public static IContainer PaddingLeft(this IContainer element, float value)
public static IContainer PaddingLeft(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Padding(x => x.Left = value);
return element.Padding(x => x.Left += value.ToPoints(unit));
}
public static IContainer PaddingRight(this IContainer element, float value)
public static IContainer PaddingRight(this IContainer element, float value, Unit unit = Unit.Point)
{
return element.Padding(x => x.Right = value);
return element.Padding(x => x.Right += value.ToPoints(unit));
}
}
}
+28 -20
View File
@@ -10,16 +10,24 @@ namespace QuestPDF.Fluent
{
internal Page Page { get; } = new Page();
public void Size(float width, float height, Unit unit = Unit.Inch)
{
var pageSize = new PageSize(width, height, unit);
MinSize(pageSize);
MaxSize(pageSize);
}
public void Size(PageSize pageSize)
{
MinSize(pageSize);
MaxSize(pageSize);
}
public void ContinuousSize(float width)
public void ContinuousSize(float width, Unit unit = Unit.Point)
{
MinSize(new PageSize(width, 0));
MaxSize(new PageSize(width, Infrastructure.Size.Max.Height));
MinSize(new PageSize(width.ToPoints(unit), 0));
MaxSize(new PageSize(width.ToPoints(unit), Infrastructure.Size.Max.Height));
}
public void MinSize(PageSize pageSize)
@@ -32,42 +40,42 @@ namespace QuestPDF.Fluent
Page.MaxSize = pageSize;
}
public void MarginLeft(float value)
public void MarginLeft(float value, Unit unit = Unit.Point)
{
Page.MarginLeft = value;
Page.MarginLeft = value.ToPoints(unit);
}
public void MarginRight(float value)
public void MarginRight(float value, Unit unit = Unit.Point)
{
Page.MarginRight = value;
Page.MarginRight = value.ToPoints(unit);
}
public void MarginTop(float value)
public void MarginTop(float value, Unit unit = Unit.Point)
{
Page.MarginTop = value;
Page.MarginTop = value.ToPoints(unit);
}
public void MarginBottom(float value)
public void MarginBottom(float value, Unit unit = Unit.Point)
{
Page.MarginBottom = value;
Page.MarginBottom = value.ToPoints(unit);
}
public void MarginVertical(float value)
public void MarginVertical(float value, Unit unit = Unit.Point)
{
MarginTop(value);
MarginBottom(value);
MarginTop(value, unit);
MarginBottom(value, unit);
}
public void MarginHorizontal(float value)
public void MarginHorizontal(float value, Unit unit = Unit.Point)
{
MarginLeft(value);
MarginRight(value);
MarginLeft(value, unit);
MarginRight(value, unit);
}
public void Margin(float value)
public void Margin(float value, Unit unit = Unit.Point)
{
MarginVertical(value);
MarginHorizontal(value);
MarginVertical(value, unit);
MarginHorizontal(value, unit);
}
public void DefaultTextStyle(TextStyle textStyle)
+35 -14
View File
@@ -6,30 +6,51 @@ namespace QuestPDF.Fluent
{
public class RowDescriptor
{
internal Row Row { get; } = new Row();
internal Row Row { get; } = new();
public void Spacing(float value)
{
Row.Spacing = value;
}
public IContainer ConstantColumn(float width)
private IContainer Item(RowItemType type, float size = 0)
{
return Column(constantWidth: width);
}
public IContainer RelativeColumn(float width = 1)
{
return Column(relativeWidth: width);
}
public IContainer Column(float constantWidth = 0, float relativeWidth = 0)
{
var element = new RowElement(constantWidth, relativeWidth);
var element = new RowItem
{
Type = type,
Size = size
};
Row.Items.Add(element);
return element;
}
[Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")]
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.")]
public IContainer ConstantColumn(float size)
{
return Item(RowItemType.Constant, size);
}
public IContainer RelativeItem(float size = 1)
{
return Item(RowItemType.Relative, size);
}
public IContainer ConstantItem(float size, Unit unit = Unit.Point)
{
return Item(RowItemType.Constant, size.ToPoints(unit));
}
public IContainer AutoItem()
{
return Item(RowItemType.Auto);
}
}
public static class RowExtensions
+2 -2
View File
@@ -21,7 +21,7 @@ namespace QuestPDF.Fluent
public static IContainer ScaleHorizontal(this IContainer element, float value)
{
return element.Scale(x => x.ScaleX = value);
return element.Scale(x => x.ScaleX *= value);
}
public static IContainer FlipHorizontal(this IContainer element)
@@ -31,7 +31,7 @@ namespace QuestPDF.Fluent
public static IContainer ScaleVertical(this IContainer element, float value)
{
return element.Scale(x => x.ScaleY = value);
return element.Scale(x => x.ScaleY *= value);
}
public static IContainer FlipVertical(this IContainer element)
-33
View File
@@ -1,33 +0,0 @@
using System;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class StackDescriptor
{
internal Stack Stack { get; } = new Stack();
public void Spacing(float value)
{
Stack.Spacing = value;
}
public IContainer Item()
{
var container = new Container();
Stack.Items.Add(container);
return container;
}
}
public static class StackExtensions
{
public static void Stack(this IContainer element, Action<StackDescriptor> handler)
{
var descriptor = new StackDescriptor();
handler(descriptor);
element.Component(descriptor.Stack);
}
}
}
+5 -5
View File
@@ -14,9 +14,9 @@ namespace QuestPDF.Fluent
{
internal List<TableColumnDefinition> Columns { get; } = new();
public void ConstantColumn(float width)
public void ConstantColumn(float width, Unit unit = Unit.Point)
{
ComplexColumn(constantWidth: width);
ComplexColumn(constantWidth: width.ToPoints(unit));
}
public void RelativeColumn(float width = 1)
@@ -24,7 +24,7 @@ namespace QuestPDF.Fluent
ComplexColumn(relativeWidth: width);
}
public void ComplexColumn(float constantWidth = 0, float relativeWidth = 0)
private void ComplexColumn(float constantWidth = 0, float relativeWidth = 0)
{
var columnDefinition = new TableColumnDefinition(constantWidth, relativeWidth);
Columns.Add(columnDefinition);
@@ -101,9 +101,9 @@ namespace QuestPDF.Fluent
container
.Decoration(decoration =>
{
decoration.Header().Element(HeaderTable);
decoration.Before().Element(HeaderTable);
decoration.Content().Element(ContentTable);
decoration.Footer().Element(FooterTable);
decoration.After().Element(FooterTable);
});
return container;
+58 -28
View File
@@ -37,9 +37,9 @@ namespace QuestPDF.Fluent
Alignment = HorizontalAlignment.Right;
}
public void ParagraphSpacing(float value)
public void ParagraphSpacing(float value, Unit unit = Unit.Point)
{
Spacing = value;
Spacing = value.ToPoints(unit);
}
private void AddItemToLastTextBlock(ITextBlockItem item)
@@ -90,57 +90,81 @@ namespace QuestPDF.Fluent
Span(Environment.NewLine);
}
private void PageNumber(string slotName, TextStyle? style = null)
private static Func<int?, string> DefaultTextFormat = x => (x ?? 123).ToString();
private void PageNumber(Func<IPageContext, int?> pageNumber, TextStyle? style, Func<int?, string>? format)
{
if (IsNullOrEmpty(slotName))
throw new ArgumentException(nameof(slotName));
style ??= TextStyle.Default;
format ??= DefaultTextFormat;
AddItemToLastTextBlock(new TextBlockPageNumber()
{
Style = style,
SlotName = slotName
Source = context => format(pageNumber(context)),
Style = style
});
}
public void CurrentPageNumber(TextStyle? style = null)
public void CurrentPageNumber(TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(PageContext.CurrentPageSlot, style);
PageNumber(x => x.CurrentPage, style, format);
}
public void TotalPages(TextStyle? style = null)
public void TotalPages(TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(PageContext.TotalPagesSlot, style);
PageNumber(x => x.GetLocation(PageContext.DocumentLocation)?.Length, style, format);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the BeginPageNumberOfSection method.")]
public void PageNumberOfLocation(string locationName, TextStyle? style = null)
{
if (IsNullOrEmpty(locationName))
throw new ArgumentException(nameof(locationName));
PageNumber(locationName, style);
BeginPageNumberOfSection(locationName);
}
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
public void BeginPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(x => x.GetLocation(locationName)?.PageStart, style, format);
}
public void EndPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(x => x.GetLocation(locationName)?.PageEnd, style, format);
}
public void PageNumberWithinSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd, style, format);
}
public void TotalPagesWithinSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
{
PageNumber(x => x.GetLocation(locationName)?.Length, style, format);
}
public void SectionLink(string? text, string sectionName, TextStyle? style = null)
{
if (IsNullOrEmpty(text))
return;
if (IsNullOrEmpty(locationName))
throw new ArgumentException(nameof(locationName));
if (IsNullOrEmpty(sectionName))
throw new ArgumentException(nameof(sectionName));
style ??= TextStyle.Default;
AddItemToLastTextBlock(new TextBlockInternalLink
AddItemToLastTextBlock(new TextBlockSectionLink
{
Style = style,
Text = text,
LocationName = locationName
SectionName = sectionName
});
}
public void ExternalLocation(string? text, string url, TextStyle? style = null)
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
{
SectionLink(text, locationName, style);
}
public void Hyperlink(string? text, string url, TextStyle? style = null)
{
if (IsNullOrEmpty(text))
return;
@@ -150,7 +174,7 @@ namespace QuestPDF.Fluent
style ??= TextStyle.Default;
AddItemToLastTextBlock(new TextBlockExternalLink
AddItemToLastTextBlock(new TextBlockHyperlink
{
Style = style,
Text = text,
@@ -158,6 +182,12 @@ namespace QuestPDF.Fluent
});
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
public void ExternalLocation(string? text, string url, TextStyle? style = null)
{
Hyperlink(text, url, style);
}
public IContainer Element()
{
var container = new Container();
@@ -174,12 +204,12 @@ namespace QuestPDF.Fluent
{
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
container.DefaultTextStyle(DefaultStyle).Stack(stack =>
container.DefaultTextStyle(DefaultStyle).Column(column =>
{
stack.Spacing(Spacing);
column.Spacing(Spacing);
foreach (var textBlock in TextBlocks)
stack.Item().Element(textBlock);
column.Item().Element(textBlock);
});
}
}

Some files were not shown because too many files have changed in this diff Show More