Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97e4572c39 |
@@ -1,70 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -13,11 +13,11 @@ namespace QuestPDF.Examples
|
|||||||
[Test]
|
[Test]
|
||||||
public void Example()
|
public void Example()
|
||||||
{
|
{
|
||||||
FontManager.RegisterFontType(File.OpenRead("LibreBarcode39-Regular.ttf"));
|
FontManager.RegisterFontType("LibreBarcode39", File.OpenRead("LibreBarcode39-Regular.ttf"));
|
||||||
|
|
||||||
RenderingTest
|
RenderingTest
|
||||||
.Create()
|
.Create()
|
||||||
.PageSize(400, 200)
|
.PageSize(400, 100)
|
||||||
.ShowResults()
|
.ShowResults()
|
||||||
.Render(container =>
|
.Render(container =>
|
||||||
{
|
{
|
||||||
@@ -25,7 +25,7 @@ namespace QuestPDF.Examples
|
|||||||
.Background(Colors.White)
|
.Background(Colors.White)
|
||||||
.AlignCenter()
|
.AlignCenter()
|
||||||
.AlignMiddle()
|
.AlignMiddle()
|
||||||
.Text("*QuestPDF*", TextStyle.Default.FontType("Libre Barcode 39").Size(64));
|
.Text("*QuestPDF*", TextStyle.Default.FontType("LibreBarcode39").Size(64));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using NUnit.Framework;
|
||||||
using System.Linq;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using QuestPDF.Examples.Engine;
|
using QuestPDF.Examples.Engine;
|
||||||
using QuestPDF.Fluent;
|
using QuestPDF.Fluent;
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
@@ -22,35 +20,18 @@ namespace QuestPDF.Examples
|
|||||||
{
|
{
|
||||||
container.Column(column =>
|
container.Column(column =>
|
||||||
{
|
{
|
||||||
foreach (var i in Enumerable.Range(0, 10))
|
column.Item().Element(Block);
|
||||||
column.Item().Element(Block);
|
|
||||||
|
|
||||||
static void Block(IContainer container)
|
static void Block(IContainer container)
|
||||||
{
|
{
|
||||||
container
|
container
|
||||||
.Width(72)
|
.Width(72)
|
||||||
.Height(3.5f, Unit.Inch)
|
.Height(3.5f)
|
||||||
.Height(1.5f, Unit.Inch)
|
.Height(1.5f)
|
||||||
.Background(Placeholders.BackgroundColor());
|
.Background(Placeholders.BackgroundColor());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Stability_NoItems()
|
|
||||||
{
|
|
||||||
RenderingTest
|
|
||||||
.Create()
|
|
||||||
.ProducePdf()
|
|
||||||
.MaxPages(100)
|
|
||||||
.PageSize(250, 150)
|
|
||||||
.Render(container =>
|
|
||||||
{
|
|
||||||
container
|
|
||||||
.Padding(25)
|
|
||||||
.Column(column => { });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using QuestPDF.Examples.Engine;
|
||||||
|
using QuestPDF.Fluent;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
|
namespace QuestPDF.Examples
|
||||||
|
{
|
||||||
|
internal class MyComponent : IComponent
|
||||||
|
{
|
||||||
|
public ISlot Header { get; set; }
|
||||||
|
public ISlot<string> Content { get; set; }
|
||||||
|
|
||||||
|
public void Compose(IContainer container)
|
||||||
|
{
|
||||||
|
container
|
||||||
|
.Column(column =>
|
||||||
|
{
|
||||||
|
column.Item().Slot(Header);
|
||||||
|
|
||||||
|
foreach (var i in Enumerable.Range(1, 10))
|
||||||
|
column.Item().Slot(Content, i.ToString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ComponentExamples
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void ComplexLayout()
|
||||||
|
{
|
||||||
|
RenderingTest
|
||||||
|
.Create()
|
||||||
|
.PageSize(PageSizes.A4)
|
||||||
|
.ProducePdf()
|
||||||
|
.ShowResults()
|
||||||
|
.Render(content =>
|
||||||
|
{
|
||||||
|
content
|
||||||
|
.Padding(10)
|
||||||
|
.Border(1)
|
||||||
|
.BorderColor(Colors.Grey.Medium)
|
||||||
|
.Component<MyComponent>(component =>
|
||||||
|
{
|
||||||
|
component
|
||||||
|
.Slot(x => x.Header)
|
||||||
|
.Text("This is my text");
|
||||||
|
|
||||||
|
component.Slot(x => x.Content, (input, container) =>
|
||||||
|
{
|
||||||
|
container
|
||||||
|
.Background(Placeholders.BackgroundColor())
|
||||||
|
.Padding(5)
|
||||||
|
.Text(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ using NUnit.Framework;
|
|||||||
using QuestPDF.Examples.Engine;
|
using QuestPDF.Examples.Engine;
|
||||||
using QuestPDF.Fluent;
|
using QuestPDF.Fluent;
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
|
||||||
|
|
||||||
namespace QuestPDF.Examples
|
namespace QuestPDF.Examples
|
||||||
{
|
{
|
||||||
@@ -15,20 +14,16 @@ namespace QuestPDF.Examples
|
|||||||
RenderingTest
|
RenderingTest
|
||||||
.Create()
|
.Create()
|
||||||
.PageSize(PageSizes.A5)
|
.PageSize(PageSizes.A5)
|
||||||
.ProduceImages()
|
.ProducePdf()
|
||||||
.ShowResults()
|
.ShowResults()
|
||||||
.Render(container =>
|
.Render(container =>
|
||||||
{
|
{
|
||||||
container
|
container.Padding(25).Column(column =>
|
||||||
.Padding(15)
|
{
|
||||||
.MinimalBox()
|
column.Item().Text("Above text");
|
||||||
.DefaultTextStyle(TextStyle.Default.Size(16))
|
column.Item().PaddingVertical(10).LineHorizontal(1).LineColor(Colors.Blue.Accent1);
|
||||||
.Column(column =>
|
column.Item().Text("Below text");
|
||||||
{
|
});
|
||||||
column.Item().Text("Above text");
|
|
||||||
column.Item().PaddingVertical(5).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
|
||||||
column.Item().Text("Below text");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,19 +33,17 @@ namespace QuestPDF.Examples
|
|||||||
RenderingTest
|
RenderingTest
|
||||||
.Create()
|
.Create()
|
||||||
.PageSize(PageSizes.A5)
|
.PageSize(PageSizes.A5)
|
||||||
.ProduceImages()
|
.ProducePdf()
|
||||||
.ShowResults()
|
.ShowResults()
|
||||||
.Render(container =>
|
.Render(container =>
|
||||||
{
|
{
|
||||||
container
|
container.Padding(25).Inlined(inlined =>
|
||||||
.Padding(15)
|
{
|
||||||
.DefaultTextStyle(TextStyle.Default.Size(16))
|
inlined.Spacing(5);
|
||||||
.Row(row =>
|
inlined.Item().Text("Above text");
|
||||||
{
|
inlined.Item().LineVertical(1).LineColor(Colors.Blue.Accent1);
|
||||||
row.AutoItem().Text("Left text");
|
inlined.Item().Text("Below text");
|
||||||
row.AutoItem().PaddingHorizontal(10).LineVertical(1).LineColor(Colors.Grey.Medium);
|
});
|
||||||
row.AutoItem().Text("Right text");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
||||||
<PackageReference Include="Svg.Skia" Version="0.5.10" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -29,9 +28,6 @@
|
|||||||
<None Update="logo.png">
|
<None Update="logo.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
<None Update="pdf-icon.svg">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -89,21 +89,5 @@ namespace QuestPDF.Examples
|
|||||||
.ShowOnce();
|
.ShowOnce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Stability_NoItems()
|
|
||||||
{
|
|
||||||
RenderingTest
|
|
||||||
.Create()
|
|
||||||
.ProducePdf()
|
|
||||||
.MaxPages(100)
|
|
||||||
.PageSize(250, 150)
|
|
||||||
.Render(container =>
|
|
||||||
{
|
|
||||||
container
|
|
||||||
.Padding(25)
|
|
||||||
.Row(row => { });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ namespace QuestPDF.Examples
|
|||||||
RenderingTest
|
RenderingTest
|
||||||
.Create()
|
.Create()
|
||||||
.PageSize(PageSizes.A4)
|
.PageSize(PageSizes.A4)
|
||||||
.ProduceImages()
|
.ProducePdf()
|
||||||
.ShowResults()
|
.ShowResults()
|
||||||
.Render(container =>
|
.Render(container =>
|
||||||
{
|
{
|
||||||
@@ -23,15 +23,15 @@ namespace QuestPDF.Examples
|
|||||||
{
|
{
|
||||||
var text = Placeholders.Paragraph();
|
var text = Placeholders.Paragraph();
|
||||||
|
|
||||||
foreach (var i in Enumerable.Range(2, 5))
|
foreach (var i in Enumerable.Range(0, 16))
|
||||||
{
|
{
|
||||||
column
|
column
|
||||||
.Item()
|
.Item()
|
||||||
.MinimalBox()
|
.MinimalBox()
|
||||||
.Border(1)
|
.Border(1)
|
||||||
.Padding(5)
|
.Padding(5)
|
||||||
.Width(i * 40)
|
.Width(50 + i * 25)
|
||||||
.Height(i * 20)
|
.Height(25 + i * 5)
|
||||||
.ScaleToFit()
|
.ScaleToFit()
|
||||||
.Text(text);
|
.Text(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -49,39 +49,6 @@ 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]
|
[Test]
|
||||||
public void DefaultCellStyle()
|
public void DefaultCellStyle()
|
||||||
{
|
{
|
||||||
@@ -250,7 +217,7 @@ namespace QuestPDF.Examples
|
|||||||
RenderingTest
|
RenderingTest
|
||||||
.Create()
|
.Create()
|
||||||
.ProduceImages()
|
.ProduceImages()
|
||||||
.PageSize(220, 170)
|
.PageSize(220, 220)
|
||||||
.ShowResults()
|
.ShowResults()
|
||||||
.Render(container =>
|
.Render(container =>
|
||||||
{
|
{
|
||||||
@@ -413,7 +380,7 @@ namespace QuestPDF.Examples
|
|||||||
.EnableCaching()
|
.EnableCaching()
|
||||||
.EnableDebugging(false)
|
.EnableDebugging(false)
|
||||||
.ShowResults()
|
.ShowResults()
|
||||||
.Render(container => GeneratePerformanceStructure(container, 250));
|
.Render(container => GeneratePerformanceStructure(container, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void GeneratePerformanceStructure(IContainer container, int repeats)
|
public static void GeneratePerformanceStructure(IContainer container, int repeats)
|
||||||
@@ -489,10 +456,8 @@ namespace QuestPDF.Examples
|
|||||||
return container
|
return container
|
||||||
.Border(1)
|
.Border(1)
|
||||||
.Background(Colors.Grey.Lighten3)
|
.Background(Colors.Grey.Lighten3)
|
||||||
.ShowOnce()
|
|
||||||
.MinWidth(50)
|
.MinWidth(50)
|
||||||
.MinHeight(50)
|
.MinHeight(50)
|
||||||
.Padding(10)
|
|
||||||
.AlignCenter()
|
.AlignCenter()
|
||||||
.AlignMiddle();
|
.AlignMiddle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ namespace QuestPDF.Examples
|
|||||||
.Padding(10)
|
.Padding(10)
|
||||||
.Text(text =>
|
.Text(text =>
|
||||||
{
|
{
|
||||||
text.DefaultTextStyle(TextStyle.Default.BackgroundColor(Colors.Grey.Lighten3));
|
text.DefaultTextStyle(TextStyle.Default);
|
||||||
text.AlignLeft();
|
text.AlignLeft();
|
||||||
text.ParagraphSpacing(10);
|
text.ParagraphSpacing(10);
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ namespace QuestPDF.Examples
|
|||||||
|
|
||||||
text.EmptyLine();
|
text.EmptyLine();
|
||||||
|
|
||||||
foreach (var i in Enumerable.Range(1, 1000))
|
foreach (var i in Enumerable.Range(1, 100))
|
||||||
{
|
{
|
||||||
text.Line($"{i}: {Placeholders.Paragraph()}");
|
text.Line($"{i}: {Placeholders.Paragraph()}");
|
||||||
|
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
<?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 "PDF":
|
|
||||||
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>
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
@@ -25,32 +24,5 @@ namespace QuestPDF.ReportSample
|
|||||||
Latitude = Helpers.Random.NextDouble() * 180f - 90f
|
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,9 +39,9 @@ namespace QuestPDF.ReportSample.Layouts
|
|||||||
|
|
||||||
page.Footer().AlignCenter().Text(text =>
|
page.Footer().AlignCenter().Text(text =>
|
||||||
{
|
{
|
||||||
text.CurrentPageNumber(format: x => x?.FormatAsRomanNumeral() ?? "-----");
|
text.CurrentPageNumber();
|
||||||
text.Span(" / ");
|
text.Span(" / ");
|
||||||
text.TotalPages(format: x => x?.FormatAsRomanNumeral() ?? "-----");
|
text.TotalPages();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
|
||||||
using QuestPDF.Fluent;
|
using QuestPDF.Fluent;
|
||||||
using QuestPDF.Helpers;
|
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace QuestPDF.ReportSample.Layouts
|
namespace QuestPDF.ReportSample.Layouts
|
||||||
@@ -45,18 +43,7 @@ namespace QuestPDF.ReportSample.Layouts
|
|||||||
{
|
{
|
||||||
row.ConstantItem(25).Text($"{number}.");
|
row.ConstantItem(25).Text($"{number}.");
|
||||||
row.RelativeItem().Text(locationName);
|
row.RelativeItem().Text(locationName);
|
||||||
row.ConstantItem(150).AlignRight().Text(text =>
|
row.ConstantItem(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName));
|
||||||
{
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
|
|||||||
public class ExternalLinkTests
|
public class ExternalLinkTests
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
public void Measure() => SimpleContainerTests.Measure<Hyperlink>();
|
public void Measure() => SimpleContainerTests.Measure<ExternalLink>();
|
||||||
|
|
||||||
// TODO: consider tests for the Draw method
|
// TODO: consider tests for the Draw method
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
|
|||||||
public class InternalLinkTests
|
public class InternalLinkTests
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
public void Measure() => SimpleContainerTests.Measure<SectionLink>();
|
public void Measure() => SimpleContainerTests.Measure<InternalLink>();
|
||||||
|
|
||||||
// TODO: consider tests for the Draw method
|
// TODO: consider tests for the Draw method
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
|
|||||||
public class InternalLocationTests
|
public class InternalLocationTests
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
public void Measure() => SimpleContainerTests.Measure<SectionLink>();
|
public void Measure() => SimpleContainerTests.Measure<InternalLink>();
|
||||||
|
|
||||||
// TODO: consider tests for the Draw method
|
// TODO: consider tests for the Draw method
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ namespace QuestPDF.UnitTests.TestEngine
|
|||||||
|
|
||||||
public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
|
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 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 DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
|
||||||
|
|
||||||
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
|
||||||
public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
|
public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
|
||||||
public void DrawSection(string sectionName) => throw new NotImplementedException();
|
public void DrawLocation(string locationName) => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,11 +16,10 @@ namespace QuestPDF.UnitTests.TestEngine
|
|||||||
|
|
||||||
public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
|
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 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 DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
|
||||||
|
|
||||||
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
|
||||||
public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
|
public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
|
||||||
public void DrawSection(string sectionName) => throw new NotImplementedException();
|
public void DrawLocation(string locationName) => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,52 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using SkiaSharp.HarfBuzz;
|
|
||||||
|
|
||||||
namespace QuestPDF.Drawing
|
namespace QuestPDF.Drawing
|
||||||
{
|
{
|
||||||
public static class FontManager
|
public static class FontManager
|
||||||
{
|
{
|
||||||
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
private static ConcurrentDictionary<string, SKTypeface> Typefaces = new ConcurrentDictionary<string, SKTypeface>();
|
||||||
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
|
private static ConcurrentDictionary<string, SKFontMetrics> FontMetrics = new ConcurrentDictionary<string, SKFontMetrics>();
|
||||||
private static ConcurrentDictionary<object, SKPaint> Paints = new();
|
private static ConcurrentDictionary<string, SKPaint> Paints = new ConcurrentDictionary<string, SKPaint>();
|
||||||
private static ConcurrentDictionary<object, SKFont> Fonts = new();
|
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new ConcurrentDictionary<string, SKPaint>();
|
||||||
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)
|
public static void RegisterFontType(string fontName, Stream stream)
|
||||||
{
|
{
|
||||||
using var fontData = SKData.Create(stream);
|
Typefaces.TryAdd(fontName, SKTypeface.FromStream(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)
|
internal static SKPaint ColorToPaint(this string color)
|
||||||
{
|
{
|
||||||
return ColorPaint.GetOrAdd(color, Convert);
|
return ColorPaint.GetOrAdd(color, Convert);
|
||||||
@@ -59,57 +30,37 @@ namespace QuestPDF.Drawing
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static SKPaint ToPaint(this TextStyle style)
|
internal static SKPaint ToPaint(this TextStyle style)
|
||||||
{
|
{
|
||||||
return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
|
return Paints.GetOrAdd(style.Key, key => Convert(style));
|
||||||
|
|
||||||
static SKPaint Convert(TextStyle style)
|
static SKPaint Convert(TextStyle style)
|
||||||
{
|
{
|
||||||
return new SKPaint
|
return new SKPaint
|
||||||
{
|
{
|
||||||
Color = SKColor.Parse(style.Color),
|
Color = SKColor.Parse(style.Color),
|
||||||
Typeface = GetTypeface(style),
|
Typeface = GetTypeface(style),
|
||||||
TextSize = style.Size ?? 12
|
TextSize = (style.Size ?? 12),
|
||||||
|
TextEncoding = SKTextEncoding.Utf32
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static SKTypeface GetTypeface(TextStyle style)
|
static SKTypeface GetTypeface(TextStyle style)
|
||||||
{
|
{
|
||||||
var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
|
if (Typefaces.TryGetValue(style.FontType, out var result))
|
||||||
|
return result;
|
||||||
|
|
||||||
var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
|
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);
|
|
||||||
|
|
||||||
if (fontFromDefaultSource != null)
|
return SKTypeface.FromFamilyName(style.FontType, (int)(style.FontWeight ?? FontWeight.Normal), (int)SKFontStyleWidth.Normal, slant)
|
||||||
return fontFromDefaultSource;
|
?? throw new ArgumentException($"The typeface {style.FontType} could not be found.");
|
||||||
|
|
||||||
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)
|
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
|
||||||
{
|
{
|
||||||
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
|
return FontMetrics.GetOrAdd(style.Key, key => style.ToPaint().FontMetrics);
|
||||||
}
|
|
||||||
|
|
||||||
internal static SKShaper ToShaper(this TextStyle style)
|
|
||||||
{
|
|
||||||
return Shapers.GetOrAdd(style.FontMetricsKey, _ => new SKShaper(style.ToFont().Typeface));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,11 +44,6 @@ namespace QuestPDF.Drawing
|
|||||||
public void DrawText(string text, Position position, TextStyle style)
|
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)
|
public void DrawImage(SKImage image, Position position, Size size)
|
||||||
@@ -56,17 +51,17 @@ namespace QuestPDF.Drawing
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawHyperlink(string url, Size size)
|
public void DrawExternalLink(string url, Size size)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawSectionLink(string sectionName, Size size)
|
public void DrawLocationLink(string locationName, Size size)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawSection(string sectionName)
|
public void DrawLocation(string locationName)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using SkiaSharp.HarfBuzz;
|
|
||||||
|
|
||||||
namespace QuestPDF.Drawing
|
namespace QuestPDF.Drawing
|
||||||
{
|
{
|
||||||
@@ -33,29 +32,24 @@ namespace QuestPDF.Drawing
|
|||||||
Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
|
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)
|
public void DrawImage(SKImage image, Position vector, Size size)
|
||||||
{
|
{
|
||||||
Canvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height));
|
Canvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawHyperlink(string url, Size size)
|
public void DrawExternalLink(string url, Size size)
|
||||||
{
|
{
|
||||||
Canvas.DrawUrlAnnotation(new SKRect(0, 0, size.Width, size.Height), url);
|
Canvas.DrawUrlAnnotation(new SKRect(0, 0, size.Width, size.Height), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawSectionLink(string sectionName, Size size)
|
public void DrawLocationLink(string locationName, Size size)
|
||||||
{
|
{
|
||||||
Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), sectionName);
|
Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), locationName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawSection(string sectionName)
|
public void DrawLocation(string locationName)
|
||||||
{
|
{
|
||||||
Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), sectionName);
|
Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), locationName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Rotate(float angle)
|
public void Rotate(float angle)
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
if (!Items.Any())
|
|
||||||
return SpacePlan.FullRender(Size.Zero);
|
|
||||||
|
|
||||||
var renderingCommands = PlanLayout(availableSpace);
|
var renderingCommands = PlanLayout(availableSpace);
|
||||||
|
|
||||||
if (!renderingCommands.Any())
|
if (!renderingCommands.Any())
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
|
|||||||
|
|
||||||
namespace QuestPDF.Elements
|
namespace QuestPDF.Elements
|
||||||
{
|
{
|
||||||
internal class Hyperlink : ContainerElement
|
internal class ExternalLink : ContainerElement
|
||||||
{
|
{
|
||||||
public string Url { get; set; } = "https://www.questpdf.com";
|
public string Url { get; set; } = "https://www.questpdf.com";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
|
|||||||
if (targetSize.Type == SpacePlanType.Wrap)
|
if (targetSize.Type == SpacePlanType.Wrap)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Canvas.DrawHyperlink(Url, targetSize);
|
Canvas.DrawExternalLink(Url, targetSize);
|
||||||
base.Draw(availableSpace);
|
base.Draw(availableSpace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,9 @@ using QuestPDF.Infrastructure;
|
|||||||
|
|
||||||
namespace QuestPDF.Elements
|
namespace QuestPDF.Elements
|
||||||
{
|
{
|
||||||
internal class SectionLink : ContainerElement
|
internal class InternalLink : ContainerElement
|
||||||
{
|
{
|
||||||
public string SectionName { get; set; }
|
public string LocationName { get; set; }
|
||||||
|
|
||||||
internal override void Draw(Size availableSpace)
|
internal override void Draw(Size availableSpace)
|
||||||
{
|
{
|
||||||
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
|
|||||||
if (targetSize.Type == SpacePlanType.Wrap)
|
if (targetSize.Type == SpacePlanType.Wrap)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Canvas.DrawSectionLink(SectionName, targetSize);
|
Canvas.DrawLocationLink(LocationName, targetSize);
|
||||||
base.Draw(availableSpace);
|
base.Draw(availableSpace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace QuestPDF.Elements
|
namespace QuestPDF.Elements
|
||||||
{
|
{
|
||||||
internal class Section : ContainerElement, IStateResettable
|
internal class InternalLocation : ContainerElement, IStateResettable
|
||||||
{
|
{
|
||||||
public string LocationName { get; set; }
|
public string LocationName { get; set; }
|
||||||
private bool IsRendered { get; set; }
|
private bool IsRendered { get; set; }
|
||||||
@@ -16,11 +16,12 @@ namespace QuestPDF.Elements
|
|||||||
{
|
{
|
||||||
if (!IsRendered)
|
if (!IsRendered)
|
||||||
{
|
{
|
||||||
Canvas.DrawSection(LocationName);
|
PageContext.SetLocationPage(LocationName);
|
||||||
|
|
||||||
|
Canvas.DrawLocation(LocationName);
|
||||||
IsRendered = true;
|
IsRendered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
PageContext.SetSectionPage(LocationName);
|
|
||||||
base.Draw(availableSpace);
|
base.Draw(availableSpace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,9 +53,6 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
if (!Items.Any())
|
|
||||||
return SpacePlan.FullRender(Size.Zero);
|
|
||||||
|
|
||||||
UpdateItemsWidth(availableSpace.Width);
|
UpdateItemsWidth(availableSpace.Width);
|
||||||
var renderingCommands = PlanLayout(availableSpace);
|
var renderingCommands = PlanLayout(availableSpace);
|
||||||
|
|
||||||
@@ -77,9 +74,6 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
internal override void Draw(Size availableSpace)
|
internal override void Draw(Size availableSpace)
|
||||||
{
|
{
|
||||||
if (!Items.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
UpdateItemsWidth(availableSpace.Width);
|
UpdateItemsWidth(availableSpace.Width);
|
||||||
var renderingCommands = PlanLayout(availableSpace);
|
var renderingCommands = PlanLayout(availableSpace);
|
||||||
|
|
||||||
@@ -149,12 +143,8 @@ namespace QuestPDF.Elements
|
|||||||
renderingCommands.Add(command);
|
renderingCommands.Add(command);
|
||||||
leftOffset += item.Width + Spacing;
|
leftOffset += item.Width + Spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rowHeight = renderingCommands
|
var rowHeight = renderingCommands.Where(x => !x.RowItem.IsRendered).Max(x => x.Measurement.Height);
|
||||||
.Where(x => !x.RowItem.IsRendered)
|
|
||||||
.Select(x => x.Measurement.Height)
|
|
||||||
.DefaultIfEmpty(0)
|
|
||||||
.Max();
|
|
||||||
|
|
||||||
foreach (var command in renderingCommands)
|
foreach (var command in renderingCommands)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,19 +15,15 @@ namespace QuestPDF.Elements.Table
|
|||||||
public List<TableCell> Cells { get; set; } = new List<TableCell>();
|
public List<TableCell> Cells { get; set; } = new List<TableCell>();
|
||||||
public bool ExtendLastCellsToTableBottom { get; set; }
|
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 StartingRowsCount { get; set; }
|
||||||
private int RowsCount { get; set; }
|
private int RowsCount { get; set; }
|
||||||
private int CurrentRow { 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()
|
internal override IEnumerable<Element?> GetChildren()
|
||||||
{
|
{
|
||||||
return Cells;
|
return Cells;
|
||||||
@@ -35,6 +31,24 @@ namespace QuestPDF.Elements.Table
|
|||||||
|
|
||||||
public void ResetState()
|
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);
|
Cells.ForEach(x => x.IsRendered = false);
|
||||||
CurrentRow = 1;
|
CurrentRow = 1;
|
||||||
}
|
}
|
||||||
@@ -71,9 +85,6 @@ namespace QuestPDF.Elements.Table
|
|||||||
{
|
{
|
||||||
if (command.Measurement.Type == SpacePlanType.FullRender)
|
if (command.Measurement.Type == SpacePlanType.FullRender)
|
||||||
command.Cell.IsRendered = true;
|
command.Cell.IsRendered = true;
|
||||||
|
|
||||||
if (command.Measurement.Type == SpacePlanType.Wrap)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Canvas.Translate(command.Offset);
|
Canvas.Translate(command.Offset);
|
||||||
command.Cell.Draw(command.Size);
|
command.Cell.Draw(command.Size);
|
||||||
@@ -92,7 +103,7 @@ namespace QuestPDF.Elements.Table
|
|||||||
{
|
{
|
||||||
return commands
|
return commands
|
||||||
.GroupBy(x => x.Cell.Row)
|
.GroupBy(x => x.Cell.Row)
|
||||||
.Where(x => x.All(y => y.Cell.IsRendered || y.Measurement.Type == SpacePlanType.FullRender))
|
.Where(x => x.All(y => y.Measurement.Type == SpacePlanType.FullRender))
|
||||||
.Select(x => x.Key)
|
.Select(x => x.Key)
|
||||||
.DefaultIfEmpty(0)
|
.DefaultIfEmpty(0)
|
||||||
.Max();
|
.Max();
|
||||||
@@ -141,12 +152,16 @@ namespace QuestPDF.Elements.Table
|
|||||||
ICollection<TableCellRenderingCommand> GetRenderingCommands()
|
ICollection<TableCellRenderingCommand> GetRenderingCommands()
|
||||||
{
|
{
|
||||||
var rowBottomOffsets = new DynamicDictionary<int, float>();
|
var rowBottomOffsets = new DynamicDictionary<int, float>();
|
||||||
var commands = new List<TableCellRenderingCommand>();
|
|
||||||
|
|
||||||
var cellsToTry = Cells.Where(x => x.Row + x.RowSpan - 1 >= CurrentRow);
|
var cellsToTry = Enumerable
|
||||||
|
.Range(CurrentRow, RowsCount - CurrentRow + 1)
|
||||||
|
.SelectMany(x => OrderedCells[x]);
|
||||||
|
|
||||||
var currentRow = CurrentRow;
|
var currentRow = CurrentRow;
|
||||||
var maxRenderingRow = RowsCount;
|
var maxRenderingRow = RowsCount;
|
||||||
|
|
||||||
|
var commands = new List<TableCellRenderingCommand>();
|
||||||
|
|
||||||
foreach (var cell in cellsToTry)
|
foreach (var cell in cellsToTry)
|
||||||
{
|
{
|
||||||
// update position of previous row
|
// update position of previous row
|
||||||
@@ -166,7 +181,10 @@ namespace QuestPDF.Elements.Table
|
|||||||
// cell visibility optimizations
|
// cell visibility optimizations
|
||||||
if (cell.Row > maxRenderingRow)
|
if (cell.Row > maxRenderingRow)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
if (cell.IsRendered)
|
||||||
|
continue;
|
||||||
|
|
||||||
// calculate cell position / size
|
// calculate cell position / size
|
||||||
var topOffset = rowBottomOffsets[cell.Row - 1];
|
var topOffset = rowBottomOffsets[cell.Row - 1];
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
namespace QuestPDF.Elements.Text.Calculation
|
namespace QuestPDF.Elements.Text.Calculation
|
||||||
{
|
{
|
||||||
internal struct TextDrawingRequest
|
internal class 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 float TotalAscent { get; set; }
|
||||||
public Size TextSize { get; set; }
|
public Size TextSize { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ namespace QuestPDF.Elements.Text.Calculation
|
|||||||
internal class TextLineElement
|
internal class TextLineElement
|
||||||
{
|
{
|
||||||
public ITextBlockItem Item { get; set; }
|
public ITextBlockItem Item { get; set; }
|
||||||
public TextBlockSize Measurement { get; set; }
|
public TextMeasurementResult Measurement { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace QuestPDF.Elements.Text.Calculation
|
namespace QuestPDF.Elements.Text.Calculation
|
||||||
{
|
{
|
||||||
internal class TextBlockSize
|
internal class TextMeasurementResult
|
||||||
{
|
{
|
||||||
public float Width { get; set; }
|
public float Width { get; set; }
|
||||||
public float Height => Math.Abs(Descent) + Math.Abs(Ascent);
|
public float Height => Math.Abs(Descent) + Math.Abs(Ascent);
|
||||||
@@ -11,5 +11,12 @@ namespace QuestPDF.Elements.Text.Calculation
|
|||||||
public float Descent { get; set; }
|
public float Descent { get; set; }
|
||||||
|
|
||||||
public float LineHeight { 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,7 @@ namespace QuestPDF.Elements.Text.Items
|
|||||||
{
|
{
|
||||||
internal interface ITextBlockItem
|
internal interface ITextBlockItem
|
||||||
{
|
{
|
||||||
ICanvas Canvas { get; set; }
|
TextMeasurementResult? Measure(TextMeasurementRequest request);
|
||||||
IPageContext PageContext { get; set; }
|
|
||||||
|
|
||||||
TextBlockSize? Measure();
|
|
||||||
void Draw(TextDrawingRequest request);
|
void Draw(TextDrawingRequest request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,40 +7,41 @@ namespace QuestPDF.Elements.Text.Items
|
|||||||
{
|
{
|
||||||
internal class TextBlockElement : ITextBlockItem
|
internal class TextBlockElement : ITextBlockItem
|
||||||
{
|
{
|
||||||
public ICanvas Canvas { get; set; }
|
|
||||||
public IPageContext PageContext { get; set; }
|
|
||||||
|
|
||||||
public Element Element { get; set; } = Empty.Instance;
|
public Element Element { get; set; } = Empty.Instance;
|
||||||
|
|
||||||
public TextBlockSize? Measure()
|
public TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||||
{
|
{
|
||||||
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
|
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
|
||||||
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
|
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
|
||||||
|
|
||||||
var measurement = Element.Measure(Size.Max);
|
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
|
||||||
|
|
||||||
if (measurement.Type != SpacePlanType.FullRender)
|
if (measurement.Type != SpacePlanType.FullRender)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new TextBlockSize
|
return new TextMeasurementResult
|
||||||
{
|
{
|
||||||
Width = measurement.Width,
|
Width = measurement.Width,
|
||||||
|
|
||||||
Ascent = -measurement.Height,
|
Ascent = -measurement.Height,
|
||||||
Descent = 0,
|
Descent = 0,
|
||||||
|
|
||||||
LineHeight = 1
|
LineHeight = 1,
|
||||||
|
|
||||||
|
StartIndex = 0,
|
||||||
|
EndIndex = 0,
|
||||||
|
TotalIndex = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(TextDrawingRequest request)
|
public void Draw(TextDrawingRequest request)
|
||||||
{
|
{
|
||||||
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
|
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
|
||||||
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
|
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
|
||||||
|
|
||||||
Canvas.Translate(new Position(0, request.TotalAscent));
|
request.Canvas.Translate(new Position(0, request.TotalAscent));
|
||||||
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
|
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
|
||||||
Canvas.Translate(new Position(0, -request.TotalAscent));
|
request.Canvas.Translate(new Position(0, -request.TotalAscent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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,29 +1,33 @@
|
|||||||
using System;
|
using QuestPDF.Elements.Text.Calculation;
|
||||||
using QuestPDF.Elements.Text.Calculation;
|
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace QuestPDF.Elements.Text.Items
|
namespace QuestPDF.Elements.Text.Items
|
||||||
{
|
{
|
||||||
internal class TextBlockPageNumber : TextBlockSpan
|
internal class TextBlockPageNumber : TextBlockSpan
|
||||||
{
|
{
|
||||||
public const string PageNumberPlaceholder = "123";
|
public string SlotName { get; set; }
|
||||||
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
|
|
||||||
|
|
||||||
public override TextBlockSize? Measure()
|
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||||
{
|
{
|
||||||
SetPageNumber();
|
SetPageNumber(request.PageContext);
|
||||||
return base.Measure();
|
return MeasureWithoutCache(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw(TextDrawingRequest request)
|
public override void Draw(TextDrawingRequest request)
|
||||||
{
|
{
|
||||||
SetPageNumber();
|
SetPageNumber(request.PageContext);
|
||||||
base.Draw(request);
|
base.Draw(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetPageNumber()
|
private void SetPageNumber(IPageContext context)
|
||||||
{
|
{
|
||||||
Text = Source(PageContext) ?? PageNumberPlaceholder;
|
var pageNumberPlaceholder = 123;
|
||||||
|
|
||||||
|
var pageNumber = context.GetRegisteredLocations().Contains(SlotName)
|
||||||
|
? context.GetLocationPage(SlotName)
|
||||||
|
: pageNumberPlaceholder;
|
||||||
|
|
||||||
|
Text = pageNumber.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +1,118 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
using QuestPDF.Elements.Text.Calculation;
|
using QuestPDF.Elements.Text.Calculation;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp.HarfBuzz;
|
|
||||||
using Size = QuestPDF.Infrastructure.Size;
|
using Size = QuestPDF.Infrastructure.Size;
|
||||||
|
|
||||||
namespace QuestPDF.Elements.Text.Items
|
namespace QuestPDF.Elements.Text.Items
|
||||||
{
|
{
|
||||||
internal class TextBlockSpan : ITextBlockItem
|
internal class TextBlockSpan : ITextBlockItem
|
||||||
{
|
{
|
||||||
public ICanvas Canvas { get; set; }
|
|
||||||
public IPageContext PageContext { get; set; }
|
|
||||||
|
|
||||||
public string Text { get; set; }
|
public string Text { get; set; }
|
||||||
public TextStyle Style { get; set; }
|
public TextStyle Style { get; set; } = new TextStyle();
|
||||||
|
|
||||||
protected TextBlockSize? Size { get; set; }
|
|
||||||
protected bool RequiresShaping { get; set; }
|
|
||||||
|
|
||||||
public virtual TextBlockSize? Measure()
|
|
||||||
{
|
|
||||||
Size ??= Text == " " ? GetSizeForSpace() : GetSizeForWord();
|
|
||||||
return Size;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextBlockSize GetSizeForWord()
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
|
||||||
|
{
|
||||||
|
const char space = ' ';
|
||||||
|
|
||||||
var paint = Style.ToPaint();
|
var paint = Style.ToPaint();
|
||||||
var fontMetrics = Style.ToFontMetrics();
|
var fontMetrics = Style.ToFontMetrics();
|
||||||
|
|
||||||
var shaper = Style.ToShaper();
|
var startIndex = request.StartIndex;
|
||||||
|
|
||||||
// shaper returns positions of all glyphs,
|
if (request.IsFirstLineElement)
|
||||||
// by adding a space, it is possible to capture width of the last original character
|
{
|
||||||
var result = shaper.Shape(Text + " ", paint);
|
while (startIndex + 1 < Text.Length && Text[startIndex] == space)
|
||||||
|
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;
|
if (Text.Length == 0)
|
||||||
|
{
|
||||||
|
return new TextMeasurementResult
|
||||||
|
{
|
||||||
|
Width = 0,
|
||||||
|
|
||||||
|
LineHeight = Style.LineHeight ?? 1,
|
||||||
|
Ascent = fontMetrics.Ascent,
|
||||||
|
Descent = fontMetrics.Descent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return new TextBlockSize
|
// start breaking text from requested position
|
||||||
|
var text = Text.Substring(startIndex);
|
||||||
|
|
||||||
|
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
|
||||||
{
|
{
|
||||||
Width = width,
|
Width = width,
|
||||||
|
|
||||||
Ascent = fontMetrics.Ascent,
|
Ascent = fontMetrics.Ascent,
|
||||||
Descent = fontMetrics.Descent,
|
Descent = fontMetrics.Descent,
|
||||||
|
|
||||||
LineHeight = Style.LineHeight ?? 1
|
LineHeight = Style.LineHeight ?? 1,
|
||||||
|
|
||||||
|
StartIndex = startIndex,
|
||||||
|
EndIndex = endIndex,
|
||||||
|
NextIndex = nextIndex,
|
||||||
|
TotalIndex = Text.Length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
public virtual void Draw(TextDrawingRequest request)
|
||||||
{
|
{
|
||||||
var fontMetrics = Style.ToFontMetrics();
|
var fontMetrics = Style.ToFontMetrics();
|
||||||
|
|
||||||
Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
|
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(Text))
|
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);
|
||||||
if (RequiresShaping)
|
|
||||||
Canvas.DrawShapedText(Text, Position.Zero, Style);
|
|
||||||
else
|
|
||||||
Canvas.DrawText(Text, Position.Zero, Style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw underline
|
// draw underline
|
||||||
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
|
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
|
||||||
@@ -94,7 +124,7 @@ namespace QuestPDF.Elements.Text.Items
|
|||||||
|
|
||||||
void DrawLine(float offset, float thickness)
|
void DrawLine(float offset, float thickness)
|
||||||
{
|
{
|
||||||
Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
|
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
using QuestPDF.Elements.Text.Calculation;
|
using QuestPDF.Elements.Text.Calculation;
|
||||||
using QuestPDF.Elements.Text.Items;
|
using QuestPDF.Elements.Text.Items;
|
||||||
@@ -12,145 +11,108 @@ namespace QuestPDF.Elements.Text
|
|||||||
internal class TextBlock : Element, IStateResettable
|
internal class TextBlock : Element, IStateResettable
|
||||||
{
|
{
|
||||||
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||||
public List<ITextBlockItem> Items { get; set; } = new();
|
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
|
||||||
|
|
||||||
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
|
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; }
|
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()
|
public void ResetState()
|
||||||
{
|
{
|
||||||
|
RenderingQueue = new Queue<ITextBlockItem>(Items);
|
||||||
CurrentElementIndex = 0;
|
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)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
if (!Items.Any())
|
if (!RenderingQueue.Any())
|
||||||
return SpacePlan.FullRender(Size.Zero);
|
return SpacePlan.FullRender(Size.Zero);
|
||||||
|
|
||||||
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
||||||
|
|
||||||
if (!lines.Any())
|
if (!lines.Any())
|
||||||
return SpacePlan.Wrap();
|
return SpacePlan.Wrap();
|
||||||
|
|
||||||
var width = lines.Max(x => x.Width);
|
var width = lines.Max(x => x.Width);
|
||||||
var height = lines.Sum(x => x.LineHeight);
|
var height = lines.Sum(x => x.LineHeight);
|
||||||
|
|
||||||
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
|
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
|
||||||
return SpacePlan.Wrap();
|
return SpacePlan.Wrap();
|
||||||
|
|
||||||
if (CurrentElementIndex + lines.Sum(x => x.Elements.Count) == Items.Count)
|
|
||||||
return SpacePlan.FullRender(width, height);
|
|
||||||
|
|
||||||
|
var fullyRenderedItemsCount = lines
|
||||||
|
.SelectMany(x => x.Elements)
|
||||||
|
.GroupBy(x => x.Item)
|
||||||
|
.Count(x => x.Any(y => y.Measurement.IsLast));
|
||||||
|
|
||||||
|
if (fullyRenderedItemsCount == RenderingQueue.Count)
|
||||||
|
return SpacePlan.FullRender(width, height);
|
||||||
|
|
||||||
return SpacePlan.PartialRender(width, height);
|
return SpacePlan.PartialRender(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override void Draw(Size availableSpace)
|
internal override void Draw(Size availableSpace)
|
||||||
{
|
{
|
||||||
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
||||||
|
|
||||||
if (!lines.Any())
|
if (!lines.Any())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var heightOffset = 0f;
|
var heightOffset = 0f;
|
||||||
var widthOffset = 0f;
|
var widthOffset = 0f;
|
||||||
|
|
||||||
var isLastPart = lines.Sum(x => x.Elements.Count) + CurrentElementIndex == Items.Count;
|
foreach (var line in lines)
|
||||||
|
|
||||||
foreach (var sourceLine in lines)
|
|
||||||
{
|
{
|
||||||
widthOffset = 0f;
|
widthOffset = 0f;
|
||||||
|
|
||||||
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);
|
var alignmentOffset = GetAlignmentOffset(line.Width);
|
||||||
|
|
||||||
Canvas.Translate(new Position(alignmentOffset, 0));
|
Canvas.Translate(new Position(alignmentOffset, 0));
|
||||||
Canvas.Translate(new Position(0, -line.Ascent));
|
Canvas.Translate(new Position(0, -line.Ascent));
|
||||||
|
|
||||||
foreach (var item in line.Elements)
|
foreach (var item in line.Elements)
|
||||||
{
|
{
|
||||||
var textDrawingRequest = new TextDrawingRequest
|
var textDrawingRequest = new TextDrawingRequest
|
||||||
{
|
{
|
||||||
|
Canvas = Canvas,
|
||||||
|
PageContext = PageContext,
|
||||||
|
|
||||||
|
StartIndex = item.Measurement.StartIndex,
|
||||||
|
EndIndex = item.Measurement.EndIndex,
|
||||||
|
|
||||||
TextSize = new Size(item.Measurement.Width, line.LineHeight),
|
TextSize = new Size(item.Measurement.Width, line.LineHeight),
|
||||||
TotalAscent = line.Ascent
|
TotalAscent = line.Ascent
|
||||||
};
|
};
|
||||||
|
|
||||||
item.Item.Draw(textDrawingRequest);
|
item.Item.Draw(textDrawingRequest);
|
||||||
|
|
||||||
Canvas.Translate(new Position(item.Measurement.Width, 0));
|
Canvas.Translate(new Position(item.Measurement.Width, 0));
|
||||||
widthOffset += item.Measurement.Width;
|
widthOffset += item.Measurement.Width;
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas.Translate(new Position(-alignmentOffset, 0));
|
Canvas.Translate(new Position(-alignmentOffset, 0));
|
||||||
Canvas.Translate(new Position(-line.Width, line.Ascent));
|
Canvas.Translate(new Position(-line.Width, line.Ascent));
|
||||||
Canvas.Translate(new Position(0, line.LineHeight));
|
Canvas.Translate(new Position(0, line.LineHeight));
|
||||||
|
|
||||||
heightOffset += line.LineHeight;
|
heightOffset += line.LineHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas.Translate(new Position(0, -heightOffset));
|
|
||||||
CurrentElementIndex += lines.Sum(x => x.Elements.Count);
|
|
||||||
|
|
||||||
if (CurrentElementIndex == Items.Count)
|
Canvas.Translate(new Position(0, -heightOffset));
|
||||||
ResetState();
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!RenderingQueue.Any())
|
||||||
|
ResetState();
|
||||||
|
|
||||||
float GetAlignmentOffset(float lineWidth)
|
float GetAlignmentOffset(float lineWidth)
|
||||||
{
|
{
|
||||||
if (Alignment == HorizontalAlignment.Left)
|
if (Alignment == HorizontalAlignment.Left)
|
||||||
@@ -168,18 +130,19 @@ namespace QuestPDF.Elements.Text
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
|
public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
|
||||||
{
|
{
|
||||||
|
var queue = new Queue<ITextBlockItem>(RenderingQueue);
|
||||||
var currentItemIndex = CurrentElementIndex;
|
var currentItemIndex = CurrentElementIndex;
|
||||||
var currentHeight = 0f;
|
var currentHeight = 0f;
|
||||||
|
|
||||||
while (true)
|
while (queue.Any())
|
||||||
{
|
{
|
||||||
var line = GetNextLine();
|
var line = GetNextLine();
|
||||||
|
|
||||||
if (!line.Elements.Any())
|
if (!line.Elements.Any())
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
|
if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
@@ -192,29 +155,43 @@ namespace QuestPDF.Elements.Text
|
|||||||
var currentWidth = 0f;
|
var currentWidth = 0f;
|
||||||
|
|
||||||
var currentLineElements = new List<TextLineElement>();
|
var currentLineElements = new List<TextLineElement>();
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (currentItemIndex == Items.Count)
|
if (!queue.Any())
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var currentElement = Items[currentItemIndex];
|
var currentElement = queue.Peek();
|
||||||
var textBlockSize = currentElement.Measure();
|
|
||||||
|
var measurementRequest = new TextMeasurementRequest
|
||||||
if (textBlockSize == null)
|
{
|
||||||
|
Canvas = Canvas,
|
||||||
|
PageContext = PageContext,
|
||||||
|
|
||||||
|
StartIndex = currentItemIndex,
|
||||||
|
AvailableWidth = availableWidth - currentWidth,
|
||||||
|
IsFirstLineElement = !currentLineElements.Any()
|
||||||
|
};
|
||||||
|
|
||||||
|
var measurementResponse = currentElement.Measure(measurementRequest);
|
||||||
|
|
||||||
|
if (measurementResponse == null)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (currentWidth + textBlockSize.Width > availableWidth + Size.Epsilon)
|
|
||||||
break;
|
|
||||||
|
|
||||||
currentLineElements.Add(new TextLineElement
|
currentLineElements.Add(new TextLineElement
|
||||||
{
|
{
|
||||||
Item = currentElement,
|
Item = currentElement,
|
||||||
Measurement = textBlockSize
|
Measurement = measurementResponse
|
||||||
});
|
});
|
||||||
|
|
||||||
currentWidth += textBlockSize.Width;
|
currentWidth += measurementResponse.Width;
|
||||||
currentItemIndex ++;
|
currentItemIndex = measurementResponse.NextIndex;
|
||||||
|
|
||||||
|
if (!measurementResponse.IsLast)
|
||||||
|
break;
|
||||||
|
|
||||||
|
currentItemIndex = 0;
|
||||||
|
queue.Dequeue();
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextLine.From(currentLineElements);
|
return TextLine.From(currentLineElements);
|
||||||
|
|||||||
@@ -1,41 +1,53 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using QuestPDF.Drawing.Exceptions;
|
using QuestPDF.Drawing.Exceptions;
|
||||||
|
using QuestPDF.Elements;
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace QuestPDF.Fluent
|
namespace QuestPDF.Fluent
|
||||||
{
|
{
|
||||||
class ComponentDescriptor<T> where T : IComponent
|
internal class ComponentDescriptor<TComponent> where TComponent : IComponent
|
||||||
{
|
{
|
||||||
public T Component { get; }
|
public TComponent Component { get; }
|
||||||
|
|
||||||
public ComponentDescriptor(T component)
|
public ComponentDescriptor(TComponent component)
|
||||||
{
|
{
|
||||||
Component = component;
|
Component = component;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IContainer Slot(Expression<Func<T, ISlot>> selector)
|
public IContainer Slot(Expression<Func<TComponent, ISlot>> selector)
|
||||||
{
|
{
|
||||||
try
|
AssureThatTheSlotIsNotConfiguredYet(selector);
|
||||||
|
|
||||||
|
var slot = new Slot();
|
||||||
|
Component.SetPropertyValue(selector, slot);
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Slot<TArgument>(Expression<Func<TComponent, ISlot<TArgument>>> selector, Action<TArgument, IContainer> handler)
|
||||||
|
{
|
||||||
|
AssureThatTheSlotIsNotConfiguredYet(selector);
|
||||||
|
|
||||||
|
var slot = new Slot<TArgument>
|
||||||
{
|
{
|
||||||
var existingValue = Component.GetPropertyValue(selector);
|
GetContent = argument =>
|
||||||
|
{
|
||||||
|
var container = new Container();
|
||||||
|
handler(argument, container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Component.SetPropertyValue(selector, slot);
|
||||||
|
}
|
||||||
|
|
||||||
if (existingValue != null)
|
private void AssureThatTheSlotIsNotConfiguredYet<TSlot>(Expression<Func<TComponent, TSlot>> selector) where TSlot : class
|
||||||
throw new DocumentComposeException($"The slot {selector.GetPropertyName()} of the component {(typeof(T).Name)} was already used.");
|
{
|
||||||
|
var existingValue = Component.GetPropertyValue(selector);
|
||||||
|
|
||||||
var slot = new Slot();
|
if (existingValue != null)
|
||||||
Component.SetPropertyValue(selector, slot);
|
throw new DocumentComposeException($"The slot {selector.GetPropertyName()} of the component {(typeof(TComponent).Name)} was already used.");
|
||||||
return slot;
|
|
||||||
}
|
|
||||||
catch (DocumentComposeException)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
throw new DocumentComposeException("Every slot in a component should be a public property with getter and setter.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +63,7 @@ namespace QuestPDF.Fluent
|
|||||||
element.Component(new T(), null);
|
element.Component(new T(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Component<T>(this IContainer element, T component, Action<ComponentDescriptor<T>>? handler = null) where T : IComponent
|
internal static void Component<T>(this IContainer element, T component, Action<ComponentDescriptor<T>>? handler = null) where T : IComponent
|
||||||
{
|
{
|
||||||
var descriptor = new ComponentDescriptor<T>(component);
|
var descriptor = new ComponentDescriptor<T>(component);
|
||||||
handler?.Invoke(descriptor);
|
handler?.Invoke(descriptor);
|
||||||
@@ -62,15 +74,21 @@ namespace QuestPDF.Fluent
|
|||||||
component.Compose(element.Container());
|
component.Compose(element.Container());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Component<T>(this IContainer element, Action<ComponentDescriptor<T>>? handler = null) where T : IComponent, new()
|
internal static void Component<T>(this IContainer element, Action<ComponentDescriptor<T>>? handler = null) where T : IComponent, new()
|
||||||
{
|
{
|
||||||
element.Component(new T(), handler);
|
element.Component(new T(), handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Slot(this IContainer element, ISlot slot)
|
internal static void Slot(this IContainer element, ISlot slot)
|
||||||
{
|
{
|
||||||
var child = (slot as Slot)?.Child;
|
var child = (slot as Slot)?.Child;
|
||||||
element.Element(child);
|
element.Element(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void Slot<TArgument>(this IContainer element, ISlot<TArgument> slot, TArgument argument)
|
||||||
|
{
|
||||||
|
var child = (slot as Slot<TArgument>)?.GetContent(argument) ?? Empty.Instance;
|
||||||
|
element.Element(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
public static void Placeholder(this IContainer element, string? text = null)
|
public static void Placeholder(this IContainer element, string? text = null)
|
||||||
{
|
{
|
||||||
element.Component(new Placeholder
|
element.Component(new Elements.Placeholder
|
||||||
{
|
{
|
||||||
Text = text ?? string.Empty
|
Text = text ?? string.Empty
|
||||||
});
|
});
|
||||||
@@ -99,45 +99,27 @@ namespace QuestPDF.Fluent
|
|||||||
return element.Element(new Container());
|
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)
|
public static IContainer ExternalLink(this IContainer element, string url)
|
||||||
{
|
{
|
||||||
return element.Hyperlink(url);
|
return element.Element(new ExternalLink
|
||||||
}
|
|
||||||
|
|
||||||
public static IContainer Hyperlink(this IContainer element, string url)
|
|
||||||
{
|
|
||||||
return element.Element(new Hyperlink
|
|
||||||
{
|
{
|
||||||
Url = url
|
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)
|
public static IContainer Location(this IContainer element, string locationName)
|
||||||
{
|
{
|
||||||
return element.Section(locationName);
|
return element.Element(new InternalLocation
|
||||||
}
|
|
||||||
|
|
||||||
public static IContainer Section(this IContainer element, string sectionName)
|
|
||||||
{
|
|
||||||
return element.Element(new Section
|
|
||||||
{
|
{
|
||||||
LocationName = sectionName
|
LocationName = locationName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
|
|
||||||
public static IContainer InternalLink(this IContainer element, string locationName)
|
public static IContainer InternalLink(this IContainer element, string locationName)
|
||||||
{
|
{
|
||||||
return element.SectionLink(locationName);
|
return element.Element(new InternalLink
|
||||||
}
|
|
||||||
|
|
||||||
public static IContainer SectionLink(this IContainer element, string sectionName)
|
|
||||||
{
|
|
||||||
return element.Element(new SectionLink
|
|
||||||
{
|
{
|
||||||
SectionName = sectionName
|
LocationName = locationName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -90,81 +90,57 @@ namespace QuestPDF.Fluent
|
|||||||
Span(Environment.NewLine);
|
Span(Environment.NewLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Func<int?, string> DefaultTextFormat = x => (x ?? 123).ToString();
|
private void PageNumber(string slotName, TextStyle? style = null)
|
||||||
|
|
||||||
private void PageNumber(Func<IPageContext, int?> pageNumber, TextStyle? style, Func<int?, string>? format)
|
|
||||||
{
|
{
|
||||||
|
if (IsNullOrEmpty(slotName))
|
||||||
|
throw new ArgumentException(nameof(slotName));
|
||||||
|
|
||||||
style ??= TextStyle.Default;
|
style ??= TextStyle.Default;
|
||||||
format ??= DefaultTextFormat;
|
|
||||||
|
|
||||||
AddItemToLastTextBlock(new TextBlockPageNumber()
|
AddItemToLastTextBlock(new TextBlockPageNumber()
|
||||||
{
|
{
|
||||||
Source = context => format(pageNumber(context)),
|
Style = style,
|
||||||
Style = style
|
SlotName = slotName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CurrentPageNumber(TextStyle? style = null, Func<int?, string>? format = null)
|
public void CurrentPageNumber(TextStyle? style = null)
|
||||||
{
|
{
|
||||||
PageNumber(x => x.CurrentPage, style, format);
|
PageNumber(PageContext.CurrentPageSlot, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TotalPages(TextStyle? style = null, Func<int?, string>? format = null)
|
public void TotalPages(TextStyle? style = null)
|
||||||
{
|
{
|
||||||
PageNumber(x => x.GetLocation(PageContext.DocumentLocation)?.Length, style, format);
|
PageNumber(PageContext.TotalPagesSlot, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This element has been renamed since version 2022.3. Please use the BeginPageNumberOfSection method.")]
|
|
||||||
public void PageNumberOfLocation(string locationName, TextStyle? style = null)
|
public void PageNumberOfLocation(string locationName, TextStyle? style = null)
|
||||||
{
|
{
|
||||||
BeginPageNumberOfSection(locationName);
|
if (IsNullOrEmpty(locationName))
|
||||||
|
throw new ArgumentException(nameof(locationName));
|
||||||
|
|
||||||
|
PageNumber(locationName, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BeginPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
public void InternalLocation(string? text, string locationName, TextStyle? style = 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))
|
if (IsNullOrEmpty(text))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (IsNullOrEmpty(sectionName))
|
if (IsNullOrEmpty(locationName))
|
||||||
throw new ArgumentException(nameof(sectionName));
|
throw new ArgumentException(nameof(locationName));
|
||||||
|
|
||||||
style ??= TextStyle.Default;
|
style ??= TextStyle.Default;
|
||||||
|
|
||||||
AddItemToLastTextBlock(new TextBlockSectionLink
|
AddItemToLastTextBlock(new TextBlockInternalLink
|
||||||
{
|
{
|
||||||
Style = style,
|
Style = style,
|
||||||
Text = text,
|
Text = text,
|
||||||
SectionName = sectionName
|
LocationName = locationName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
|
public void ExternalLocation(string? text, string url, TextStyle? style = null)
|
||||||
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))
|
if (IsNullOrEmpty(text))
|
||||||
return;
|
return;
|
||||||
@@ -174,7 +150,7 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
style ??= TextStyle.Default;
|
style ??= TextStyle.Default;
|
||||||
|
|
||||||
AddItemToLastTextBlock(new TextBlockHyperlink
|
AddItemToLastTextBlock(new TextBlockExternalLink
|
||||||
{
|
{
|
||||||
Style = style,
|
Style = style,
|
||||||
Text = text,
|
Text = text,
|
||||||
@@ -182,12 +158,6 @@ 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()
|
public IContainer Element()
|
||||||
{
|
{
|
||||||
var container = new Container();
|
var container = new Container();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
|
|||||||
{
|
{
|
||||||
public static class Placeholders
|
public static class Placeholders
|
||||||
{
|
{
|
||||||
public static readonly Random Random = new();
|
public static readonly Random Random = new Random(3); // 3
|
||||||
|
|
||||||
#region Word Cache
|
#region Word Cache
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace QuestPDF.Infrastructure
|
|||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal virtual void Initialize(IPageContext pageContext, ICanvas canvas)
|
internal void Initialize(IPageContext pageContext, ICanvas canvas)
|
||||||
{
|
{
|
||||||
PageContext = pageContext;
|
PageContext = pageContext;
|
||||||
Canvas = canvas;
|
Canvas = canvas;
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ namespace QuestPDF.Infrastructure
|
|||||||
|
|
||||||
void DrawRectangle(Position vector, Size size, string color);
|
void DrawRectangle(Position vector, Size size, string color);
|
||||||
void DrawText(string text, Position position, TextStyle style);
|
void DrawText(string text, Position position, TextStyle style);
|
||||||
void DrawShapedText(string text, Position position, TextStyle style);
|
|
||||||
void DrawImage(SKImage image, Position position, Size size);
|
void DrawImage(SKImage image, Position position, Size size);
|
||||||
|
|
||||||
void DrawHyperlink(string url, Size size);
|
void DrawExternalLink(string url, Size size);
|
||||||
void DrawSectionLink(string sectionName, Size size);
|
void DrawLocationLink(string locationName, Size size);
|
||||||
void DrawSection(string sectionName);
|
void DrawLocation(string locationName);
|
||||||
|
|
||||||
void Rotate(float angle);
|
void Rotate(float angle);
|
||||||
void Scale(float scaleX, float scaleY);
|
void Scale(float scaleX, float scaleY);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using QuestPDF.Elements;
|
using QuestPDF.Elements;
|
||||||
|
|
||||||
namespace QuestPDF.Infrastructure
|
namespace QuestPDF.Infrastructure
|
||||||
@@ -11,7 +12,17 @@ namespace QuestPDF.Infrastructure
|
|||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ISlot<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Slot<T> : ISlot<T>
|
||||||
|
{
|
||||||
|
public Func<T, IElement> GetContent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public interface IComponent
|
public interface IComponent
|
||||||
{
|
{
|
||||||
void Compose(IContainer container);
|
void Compose(IContainer container);
|
||||||
|
|||||||
@@ -2,18 +2,10 @@
|
|||||||
|
|
||||||
namespace QuestPDF.Infrastructure
|
namespace QuestPDF.Infrastructure
|
||||||
{
|
{
|
||||||
internal class DocumentLocation
|
public interface IPageContext
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
void SetLocationPage(string key);
|
||||||
public int PageStart { get; set; }
|
int GetLocationPage(string key);
|
||||||
public int PageEnd { get; set; }
|
ICollection<string> GetRegisteredLocations();
|
||||||
public int Length => PageEnd - PageStart + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface IPageContext
|
|
||||||
{
|
|
||||||
int CurrentPage { get; }
|
|
||||||
void SetSectionPage(string name);
|
|
||||||
DocumentLocation? GetLocation(string name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,44 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace QuestPDF.Infrastructure
|
namespace QuestPDF.Infrastructure
|
||||||
{
|
{
|
||||||
internal class PageContext : IPageContext
|
public class PageContext : IPageContext
|
||||||
{
|
{
|
||||||
public const string DocumentLocation = "document";
|
public const string CurrentPageSlot = "currentPage";
|
||||||
|
public const string TotalPagesSlot = "totalPages";
|
||||||
|
|
||||||
private List<DocumentLocation> Locations { get; } = new();
|
private Dictionary<string, int> Locations { get; } = new Dictionary<string, int>();
|
||||||
public int CurrentPage { get; private set; }
|
private int PageNumber { get; set; }
|
||||||
|
|
||||||
internal void SetPageNumber(int number)
|
internal void SetPageNumber(int number)
|
||||||
{
|
{
|
||||||
CurrentPage = number;
|
PageNumber = number;
|
||||||
SetSectionPage(DocumentLocation);
|
Locations[CurrentPageSlot] = number;
|
||||||
|
|
||||||
|
if (!Locations.ContainsKey(TotalPagesSlot) || Locations[TotalPagesSlot] < number)
|
||||||
|
Locations[TotalPagesSlot] = number;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetSectionPage(string name)
|
public void SetLocationPage(string key)
|
||||||
{
|
{
|
||||||
var location = GetLocation(name);
|
if (Locations.ContainsKey(key))
|
||||||
|
return;
|
||||||
if (location == null)
|
|
||||||
{
|
Locations[key] = PageNumber;
|
||||||
location = new DocumentLocation
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
PageStart = CurrentPage,
|
|
||||||
PageEnd = CurrentPage
|
|
||||||
};
|
|
||||||
|
|
||||||
Locations.Add(location);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.PageEnd < CurrentPage)
|
|
||||||
location.PageEnd = CurrentPage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DocumentLocation? GetLocation(string name)
|
public int GetLocationPage(string key)
|
||||||
{
|
{
|
||||||
return Locations.FirstOrDefault(x => x.Name == name);
|
if (!Locations.ContainsKey(key))
|
||||||
|
throw new ArgumentException($"The location '{key}' does not exists.");
|
||||||
|
|
||||||
|
return Locations[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICollection<string> GetRegisteredLocations()
|
||||||
|
{
|
||||||
|
return Locations.Keys;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,8 +17,7 @@ namespace QuestPDF.Infrastructure
|
|||||||
internal bool? HasStrikethrough { get; set; }
|
internal bool? HasStrikethrough { get; set; }
|
||||||
internal bool? HasUnderline { get; set; }
|
internal bool? HasUnderline { get; set; }
|
||||||
|
|
||||||
internal object PaintKey { get; private set; }
|
internal string? Key { get; private set; }
|
||||||
internal object FontMetricsKey { get; private set; }
|
|
||||||
|
|
||||||
internal static TextStyle LibraryDefault => new TextStyle
|
internal static TextStyle LibraryDefault => new TextStyle
|
||||||
{
|
{
|
||||||
@@ -43,8 +42,7 @@ namespace QuestPDF.Infrastructure
|
|||||||
HasGlobalStyleApplied = true;
|
HasGlobalStyleApplied = true;
|
||||||
|
|
||||||
ApplyParentStyle(globalStyle);
|
ApplyParentStyle(globalStyle);
|
||||||
PaintKey ??= (FontType, Size, FontWeight, IsItalic, Color);
|
Key ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}";
|
||||||
FontMetricsKey ??= (FontType, Size, FontWeight, IsItalic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ApplyParentStyle(TextStyle parentStyle)
|
internal void ApplyParentStyle(TextStyle parentStyle)
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ namespace QuestPDF.Infrastructure
|
|||||||
|
|
||||||
Feet,
|
Feet,
|
||||||
Inch,
|
Inch,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 1/1000th of inch
|
|
||||||
/// </summary>
|
|
||||||
Mill
|
Mill
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +32,7 @@ namespace QuestPDF.Infrastructure
|
|||||||
Point => 1,
|
Point => 1,
|
||||||
Meter => 100 / InchToCentimetre * InchToPoints,
|
Meter => 100 / InchToCentimetre * InchToPoints,
|
||||||
Centimetre => 1 / InchToCentimetre * InchToPoints,
|
Centimetre => 1 / InchToCentimetre * InchToPoints,
|
||||||
Millimetre => 0.1f / InchToCentimetre * InchToPoints,
|
Millimetre => 10 / InchToCentimetre * InchToPoints,
|
||||||
Feet => 12 * InchToPoints,
|
Feet => 12 * InchToPoints,
|
||||||
Inch => InchToPoints,
|
Inch => InchToPoints,
|
||||||
Mill => InchToPoints / 1000f,
|
Mill => InchToPoints / 1000f,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<Authors>MarcinZiabek</Authors>
|
<Authors>MarcinZiabek</Authors>
|
||||||
<Company>CodeFlint</Company>
|
<Company>CodeFlint</Company>
|
||||||
<PackageId>QuestPDF</PackageId>
|
<PackageId>QuestPDF</PackageId>
|
||||||
<Version>2022.2.3</Version>
|
<Version>2022.2.0-beta2</Version>
|
||||||
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
|
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
|
||||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
|
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||||
<LangVersion>9</LangVersion>
|
<LangVersion>9</LangVersion>
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
||||||
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -132,42 +132,41 @@ void ComposeTable(IContainer container)
|
|||||||
{
|
{
|
||||||
var headerStyle = TextStyle.Default.SemiBold();
|
var headerStyle = TextStyle.Default.SemiBold();
|
||||||
|
|
||||||
container.Table(table =>
|
container.Decoration(decoration =>
|
||||||
{
|
{
|
||||||
table.ColumnsDefinition(columns =>
|
// header
|
||||||
|
decoration.Header().BorderBottom(1).Padding(5).Row(row =>
|
||||||
{
|
{
|
||||||
columns.ConstantColumn(25);
|
row.ConstantColumn(25).Text("#", headerStyle);
|
||||||
columns.RelativeColumn(3);
|
row.RelativeColumn(3).Text("Product", headerStyle);
|
||||||
columns.RelativeColumn();
|
row.RelativeColumn().AlignRight().Text("Unit price", headerStyle);
|
||||||
columns.RelativeColumn();
|
row.RelativeColumn().AlignRight().Text("Quantity", headerStyle);
|
||||||
columns.RelativeColumn();
|
row.RelativeColumn().AlignRight().Text("Total", headerStyle);
|
||||||
});
|
});
|
||||||
|
|
||||||
table.Header(header =>
|
// content
|
||||||
{
|
decoration
|
||||||
header.Cell().Text("#", headerStyle);
|
.Content()
|
||||||
header.Cell().Text("Product", headerStyle);
|
.column(column =>
|
||||||
header.Cell().AlignRight().Text("Unit price", headerStyle);
|
|
||||||
header.Cell().AlignRight().Text("Quantity", headerStyle);
|
|
||||||
header.Cell().AlignRight().Text("Total", headerStyle);
|
|
||||||
|
|
||||||
header.Cell().ColumnSpan(5)
|
|
||||||
.PaddingVertical(5).BorderBottom(1).BorderColor(Colors.Black);
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var item in Model.Items)
|
|
||||||
{
|
|
||||||
table.Cell().Element(CellStyle).Text(Model.Items.IndexOf(item) + 1);
|
|
||||||
table.Cell().Element(CellStyle).Text(item.Name);
|
|
||||||
table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price}$");
|
|
||||||
table.Cell().Element(CellStyle).AlignRight().Text(item.Quantity);
|
|
||||||
table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price * item.Quantity}$");
|
|
||||||
|
|
||||||
static IContainer CellStyle(IContainer container)
|
|
||||||
{
|
{
|
||||||
container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5);
|
foreach (var item in Model.Items)
|
||||||
}
|
{
|
||||||
}
|
column
|
||||||
|
.Item()
|
||||||
|
.ShowEntire()
|
||||||
|
.BorderBottom(1)
|
||||||
|
.BorderColor(Colors.Grey.Lighten2)
|
||||||
|
.Padding(5)
|
||||||
|
.Row(row =>
|
||||||
|
{
|
||||||
|
row.ConstantColumn(25).Text(Model.Items.IndexOf(item) + 1);
|
||||||
|
row.RelativeColumn(3).Text(item.Name);
|
||||||
|
row.RelativeColumn().AlignRight().Text($"{item.Price}$");
|
||||||
|
row.RelativeColumn().AlignRight().Text(item.Quantity);
|
||||||
|
row.RelativeColumn().AlignRight().Text($"{item.Price * item.Quantity}$");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
- Added a `ScaleToFit` element - scales its child down so it fits in the provided space,
|
- Added a `ScaleToFit` element - scales it child down so it does fit in the provided space,
|
||||||
- Added a `StopPaging` element - when its child requires more than one page to fully render, only the first page is shown,
|
- Added a `StopPaging` element - when its child requires more than one page to fully render, only the first page is shown,
|
||||||
- Added a 'LineVertical' and a 'LineHorizontal' elements - those will simplify your code a lot, there is no need to use the `Border` element anymore!
|
- Added a 'LineVertical' and a 'LineHorizontal' elements - those will simplify your code a lot, no need to use the `Border` element anymore!
|
||||||
- Renaming: the `Stack` element was renamed to the `Column` element,
|
- Renaming: the `Stack` element has been renamed to the `Column` element,
|
||||||
- Renaming: children of the `Row` element are now called `items` instead of `columns`, e.g. `RelativeItem` instead of `RelativeColumn`,
|
- Renaming: children of the `Row` elements are not called `items` instead of `columns`, e.g. `ConstantItem`, `RelativeItem`, `AutoItem`,
|
||||||
- Added support of the `AutoItem` to the `Row` element - those items take as little width as possible,
|
- Added support of the `AutoItem` to the `Row` element - those items take as little width as possible,
|
||||||
- Improved default Fluent configuration behavior for elements: Scale, Padding, Translate,
|
- Improved default Fluent configuration behavior for elements: Scale, Padding, Translate,
|
||||||
- Improved integration support with the HttpContext.Response.Body. This improvement was introduced by schulz3000, thank you!
|
- Improved integration support with the HttpContext.Response.Body. This improvement was introduced by schulz3000, thank you!
|
||||||
@@ -2,21 +2,37 @@
|
|||||||
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/logo.svg" width="400px">
|
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/logo.svg" width="400px">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
QuestPDF presents a new approach to PDF document generation. Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in many cases is not reliable. Instead, it implements its own layouting engine that is optimized to cover all paging-related requirements. Then, everything is rendered using the SkiaSharp library (a Skia port for .NET, used in Chrome, Android, MAUI, etc.).
|
## Overview
|
||||||
|
|
||||||
I have designed this layouting engine with full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behaviour of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows.
|
**Rely on solid fundamentals** - This library is created specifically for designing and arranging document layouts, with full paging support. Alternative solutions, such as HTML-based converters, are not designed for this purpose and therefore are often unpredictable and do not produce desired results.
|
||||||
|
|
||||||
|
**Work with organized self-explanatory code** - The entire process of implementing PDF document, takes place in your code. Free yourself from slow visual designers and strange technological limitations. Follow simple yet highly effective approaches to create maintainable, high-quality code.
|
||||||
|
|
||||||
|
**Compose simple components into complex documents** - Do you remember the feeling when your code just works? When your ideas are becoming real without any effort? Working with simple, easy to understand, self-explanatory and highly composable layout elements is the key here!
|
||||||
|
|
||||||
|
**Create and reuse components** - Feel no fear of complex documents! Create custom, reusable components and divide the document's layout into easy to maintain pieces. Inject data to customize content and use slots to enhance composability. Decide how complex approaches your solution needs and follow the best path.
|
||||||
|
|
||||||
|
**Prototype with ease** - We understand that document generation is often tricky and require multiple iterations. The library offers additional prototyping tools such as random text generator or image placeholder element. By following best practices, you can develop a document without having data.
|
||||||
|
|
||||||
|
**Enjoy fast PDF generation** - QuestPDF is created upon SkiaSharp, a well-known graphical library, and converts your data into PDF documents. It offers a highly optimized layouting engine capable of generating over 1000 PDF files per minute per core. The entire process is thread-safe.
|
||||||
|
|
||||||
## Support QuestPDF
|
## Support QuestPDF
|
||||||
|
|
||||||
All great frameworks and libraries started from zero. Please help me make QuestPDF a commonly known library and an obvious choice in case of generating PDF documents. Please give it a start ⭐ and share with your colleagues 💬👨💻.
|
All great frameworks and libraries started from zero. Please help us to make QuestPDF a commonly known library and an obvious choice in case of generating PDF documents. It can be as easy as:
|
||||||
|
- Giving this repository a star ⭐ so more people will know about it,
|
||||||
|
- Observing 🤩 the library to know about each new realease,
|
||||||
|
- Trying our sample project to see how easy it is to create an invoice 📊,
|
||||||
|
- Sharing your thoughts 💬 with us and your colleagues,
|
||||||
|
- Simply using it 👨💻 and suggesting new features,
|
||||||
|
- Creating new features 🆕 for everybody.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The library is available as a nuget package. You can install it as any other nuget package from your IDE, try to search by `QuestPDF`. You can find package details [on this webpage](https://www.nuget.org/packages/QuestPDF/).
|
The library is available as a nuget package. You can install it as any other nuget package from your IDE, try to search by `QuestPDF`. You can find package details [on this webpage](https://www.nuget.org/packages/QuestPDF/).
|
||||||
|
|
||||||
```
|
<a href="https://www.nuget.org/packages/QuestPDF/">
|
||||||
Install-Package QuestPDF
|
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/nuget.svg" width="200px">
|
||||||
```
|
</a>
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -133,42 +149,41 @@ void ComposeTable(IContainer container)
|
|||||||
{
|
{
|
||||||
var headerStyle = TextStyle.Default.SemiBold();
|
var headerStyle = TextStyle.Default.SemiBold();
|
||||||
|
|
||||||
container.Table(table =>
|
container.Decoration(decoration =>
|
||||||
{
|
{
|
||||||
table.ColumnsDefinition(columns =>
|
// header
|
||||||
|
decoration.Header().BorderBottom(1).Padding(5).Row(row =>
|
||||||
{
|
{
|
||||||
columns.ConstantColumn(25);
|
row.ConstantColumn(25).Text("#", headerStyle);
|
||||||
columns.RelativeColumn(3);
|
row.RelativeColumn(3).Text("Product", headerStyle);
|
||||||
columns.RelativeColumn();
|
row.RelativeColumn().AlignRight().Text("Unit price", headerStyle);
|
||||||
columns.RelativeColumn();
|
row.RelativeColumn().AlignRight().Text("Quantity", headerStyle);
|
||||||
columns.RelativeColumn();
|
row.RelativeColumn().AlignRight().Text("Total", headerStyle);
|
||||||
});
|
});
|
||||||
|
|
||||||
table.Header(header =>
|
// content
|
||||||
{
|
decoration
|
||||||
header.Cell().Text("#", headerStyle);
|
.Content()
|
||||||
header.Cell().Text("Product", headerStyle);
|
.Stack(column =>
|
||||||
header.Cell().AlignRight().Text("Unit price", headerStyle);
|
|
||||||
header.Cell().AlignRight().Text("Quantity", headerStyle);
|
|
||||||
header.Cell().AlignRight().Text("Total", headerStyle);
|
|
||||||
|
|
||||||
header.Cell().ColumnSpan(5)
|
|
||||||
.PaddingVertical(5).BorderBottom(1).BorderColor(Colors.Black);
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var item in Model.Items)
|
|
||||||
{
|
|
||||||
table.Cell().Element(CellStyle).Text(Model.Items.IndexOf(item) + 1);
|
|
||||||
table.Cell().Element(CellStyle).Text(item.Name);
|
|
||||||
table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price}$");
|
|
||||||
table.Cell().Element(CellStyle).AlignRight().Text(item.Quantity);
|
|
||||||
table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price * item.Quantity}$");
|
|
||||||
|
|
||||||
static IContainer CellStyle(IContainer container)
|
|
||||||
{
|
{
|
||||||
container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5);
|
foreach (var item in Model.Items)
|
||||||
}
|
{
|
||||||
}
|
column
|
||||||
|
.Item()
|
||||||
|
.ShowEntire()
|
||||||
|
.BorderBottom(1)
|
||||||
|
.BorderColor(Colors.Grey.Lighten2)
|
||||||
|
.Padding(5)
|
||||||
|
.Row(row =>
|
||||||
|
{
|
||||||
|
row.ConstantColumn(25).Text(Model.Items.IndexOf(item) + 1);
|
||||||
|
row.RelativeColumn(3).Text(item.Name);
|
||||||
|
row.RelativeColumn().AlignRight().Text($"{item.Price}$");
|
||||||
|
row.RelativeColumn().AlignRight().Text(item.Quantity);
|
||||||
|
row.RelativeColumn().AlignRight().Text($"{item.Price * item.Quantity}$");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user