Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 312e94a4bd | |||
| fcef2b45d1 | |||
| 3606bc9e79 | |||
| 6fe6e7ee41 | |||
| c41d045eee | |||
| 9aa22fbeea | |||
| f1ff03fede | |||
| fc8a788cf7 | |||
| 5d2d97097c | |||
| 03e10d3d0a | |||
| 9bb1d27585 | |||
| 2b701e11d4 | |||
| 6b1de07caa | |||
| 461dcde141 | |||
| 8622633e34 | |||
| 90565650c9 | |||
| d1a7018748 | |||
| 1829ed7180 | |||
| bdb61aa545 | |||
| c21a335342 | |||
| ad47b82b8f | |||
| 2040822a37 | |||
| 6d1cd81a1d | |||
| 26195786a4 | |||
| 40603ada12 | |||
| b5d8fc403f | |||
| 84ed879026 | |||
| 3bcb0ed947 | |||
| 498a86359a | |||
| 4d1db6b76e | |||
| 6796d4a181 | |||
| 6074ed0e7e | |||
| a560c6c864 | |||
| 327c412c7d | |||
| 4b5eeea896 | |||
| ef89be9710 | |||
| 176566801b | |||
| 22a696fab9 | |||
| 9809faccb6 | |||
| 6ad465b9fa | |||
| e6c7befac4 | |||
| be3e4b59ec | |||
| e751bfd2ff | |||
| b05f76f848 | |||
| b5d7dbb346 | |||
| 0ab73f4f2b | |||
| 458b379208 | |||
| c3c0d7631c | |||
| a3ea8bceb6 | |||
| 455d187897 | |||
| 65e5d36ded | |||
| ad5e8ab27d | |||
| a17197addc | |||
| a1e7cbbeb3 | |||
| c00ac5e9c7 | |||
| 0e0a8d5d69 | |||
| bc341da6dd | |||
| 6096771b51 | |||
| 4f097d0fed | |||
| 24beea80c7 | |||
| e1e5778ff0 | |||
| 8679391609 | |||
| c1b60287ca | |||
| 011e1b7ce9 | |||
| 5fd72cbb4b | |||
| 7284998657 | |||
| 30bd7420e2 | |||
| bd48688670 | |||
| 68e3efde59 | |||
| 805fd55d94 | |||
| b3a3deb354 | |||
| 68e4a10e81 | |||
| e15a21ab0e | |||
| f851221718 | |||
| 91c93e68b5 | |||
| 3dcc35be97 | |||
| 3f32c6e2c5 | |||
| f1f609ccc1 | |||
| 5c07485cc1 | |||
| 42684ec180 | |||
| 08f9f706ce | |||
| 1f199423b0 | |||
| d8e640fcf3 | |||
| fb02d97a10 | |||
| d5f8a884d6 | |||
| 5de5be6429 | |||
| 96d57b241e | |||
| 920e61fc5e | |||
| 1cc606261c | |||
| 0a6c021fe9 | |||
| b65ee32daf | |||
| e496917d84 | |||
| a5004b986c | |||
| 84d7fcf41f | |||
| 641cc6dee2 | |||
| 9dbed14559 | |||
| 6323a8dba2 | |||
| 356dbb020d | |||
| bad6be1a28 | |||
| 27c7383437 | |||
| ecac19ee46 | |||
| fd37070d3b | |||
| 3b433f3efb | |||
| b3d6a39a98 | |||
| 8d1a449e69 | |||
| 7f0be69ffc | |||
| 8553131b99 | |||
| 1bf9198a51 | |||
| 563b782f6a | |||
| edf8db878a | |||
| 5fd6488068 | |||
| d6a7c3703d | |||
| 5844d3bdba | |||
| 011d44304a | |||
| a087be8afe | |||
| 483cbb6d46 | |||
| ec76cbd696 | |||
| a83d8c43fe | |||
| 5f25e37f7f | |||
| cafd27a60d | |||
| 5082bbbdf2 | |||
| d2ba21821f | |||
| 9bef7d64d5 | |||
| ca4352addd | |||
| 2906315fa7 | |||
| 6f036388b9 | |||
| 9cd4fd740e | |||
| c97acada76 | |||
| 19ff9062ad | |||
| e4f8243c9b | |||
| 4345ce857f | |||
| 823572bf25 | |||
| e6bd63b3ec | |||
| bbf3d71e7e | |||
| 8a57dceb9d |
@@ -0,0 +1,54 @@
|
||||
name: Build And Create Nuget Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '3 0 * * 0'
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ${{ matrix.environment }}
|
||||
strategy:
|
||||
matrix:
|
||||
environment:
|
||||
- macos-latest
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
env:
|
||||
DOTNET_NOLOGO: 1
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
UNIT_TEST_PROJECT: QuestPDF.UnitTests/QuestPDF.UnitTests.csproj
|
||||
|
||||
steps:
|
||||
- name: 📝 Fetch Sources 📝
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: ⚙ Setup .NET 6.0 SDK ⚙
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
- name: 🔄 Restore Nuget Packages 🔄
|
||||
shell: bash
|
||||
run: dotnet restore
|
||||
working-directory: ./Source
|
||||
|
||||
- name: 🛠 Build Solution 🛠
|
||||
shell: bash
|
||||
run: dotnet build -c Release --no-restore
|
||||
working-directory: ./Source
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.environment == 'windows-latest' }}
|
||||
with:
|
||||
name: Build Package
|
||||
path: |
|
||||
**/*.nupkg
|
||||
**/*.snupkg
|
||||
!.nuget
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Qodana
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'releases/*'
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Qodana Scan'
|
||||
uses: JetBrains/qodana-action@v2023.1.0
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 QuestPDF
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,13 @@
|
||||
# QuestPDF Community MIT License
|
||||
|
||||
|
||||
## License Permissions
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
## Copyright
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
|
||||
## Limitation Of Liability
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,29 @@
|
||||
# QuestPDF Professional And Enterprise Use License
|
||||
|
||||
|
||||
## Do No Harm
|
||||
By downloading or using the Software, the Licensee agrees not to utilize the software in a manner which is disparaging to QuestPDF, and not to rent, lease or otherwise transfer rights to the Software.
|
||||
|
||||
|
||||
## License Permissions
|
||||
Grants the use of the Software by a specified number of developers to create and deploy closed-source software for unlimited end user organizations ("The Organization") in multiple locations. This license covers unlimited applications or projects. The Software may be deployed upon any number of machines for the end-use of The Organization. This license also intrinsically covers for development, staging and production servers for each project.
|
||||
|
||||
Grants the use of the Software by a specified number of developers to create and deploy closed-source software for unlimited end user organizations ("The Organization") in multiple locations. This license covers unlimited applications or projects. The Software may be deployed upon any number of machines for the end-use of The Organization. This license also intrinsically covers for development, staging and production servers for each project.
|
||||
|
||||
|
||||
## License Fees
|
||||
A. If you wish to use the Software in a production environment, you may download and use the Software for one year upon payment of the appropriate license fee as indicated on the pricing page in accordance with the terms and conditions of this Agreement.
|
||||
|
||||
B. If you wish to use the Software in a non-production environment, you may download and access the source and/or binaries at no charge solely for testing and evaluation purposes and in accordance with all license limitations and restrictions set forth in this Agreement.
|
||||
|
||||
|
||||
## Ownership
|
||||
QuestPDF shall at all times retain ownership of the QuestPDF Software library and all subsequent copies.
|
||||
|
||||
|
||||
## Copyright
|
||||
Title, ownership rights, and intellectual property rights in and to the Software shall remain with QuestPDF. The Software is protected by the international copyright laws. Title, ownership rights, and intellectual property rights in and to the content accessed through the Software is the property of the applicable content owner and may be protected by applicable copyright or other law. This License gives you no rights to such content.
|
||||
|
||||
|
||||
## Limitation Of Liability
|
||||
THIS SOFTWARE IS PROVIDED "AS IS," WITHOUT A WARRANTY OF ANY KIND. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. QUESTPDF AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL QUESTPDF OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF QUESTPDF HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
@@ -0,0 +1,23 @@
|
||||
# What License Do You Need?
|
||||
|
||||
## General License Overview
|
||||
|
||||
Open-source projects are an important foundation for the software development ecosystem. Modern applications are depending more and more upon the work of enthusiast and experts. Open-source is not anymore just sharing knowledge and passion with everybody. Over last years, maintaining and developing those projects have became a highly responsible task.
|
||||
|
||||
We identify the importance of the QuestPDF library in your projects, and therefore want to make sure you can safely and confidently continue the development. We strongly believe that a dual-licensing model is the best solution for all users: providing the best development experience and feature set for everybody and every project, but also offering stability for every professional and enterprise product.
|
||||
|
||||
Being a healthy and growing community is our primary goal that motivates us to pursue professionalism.
|
||||
|
||||
[Read more about exact terms](https://www.questpdf.com/pricing.html)
|
||||
|
||||
|
||||
## QuestPDF Professional Or Enterprise Use License
|
||||
|
||||
If you are consuming the QuestPDF library as a Direct Package Dependency for usage in a Closed Source software in the capacity of a for-profit company/individual with more than 1M USD annual gross revenue, you must purchase the `QuestPDF Professional or Enterprise` License, depending on number of software developers. [Read more](https://www.questpdf.com/license-commercial.html)
|
||||
|
||||
|
||||
## QuestPDF Community MIT License
|
||||
|
||||
We love and highly appreciate the .NET Community and therefore the vast majority of users are welcome to use the library completely for free. If you do not meet the criteria described above, you are eligible to use the `QuestPDF Community MIT` License, without any limitations, even for commercial usage. [Read more](https://www.questpdf.com/license-community.html)
|
||||
|
||||
If you still want to support library development, please consider purchasing the Professional License.
|
||||
@@ -1,75 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Engines;
|
||||
using BenchmarkDotNet.Running;
|
||||
using Microsoft.VisualStudio.TestPlatform.TestHost;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Drawing.Proxy;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.ReportSample.Layouts;
|
||||
|
||||
namespace QuestPDF.ReportSample
|
||||
{
|
||||
[SimpleJob(RunStrategy.Monitoring, launchCount: 0, warmupCount: 1, targetCount: 256)]
|
||||
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
|
||||
public class PerformanceTests
|
||||
{
|
||||
private PageContext PageContext { get; set; }
|
||||
private DocumentMetadata Metadata { get; set; }
|
||||
private Container Content { get; set; }
|
||||
|
||||
[Test]
|
||||
public void Run()
|
||||
{
|
||||
ImagePlaceholder.Solid = true;
|
||||
|
||||
var configuration = ManualConfig
|
||||
.Create(DefaultConfig.Instance)
|
||||
.WithOptions(ConfigOptions.DisableOptimizationsValidator);
|
||||
|
||||
BenchmarkRunner.Run<PerformanceTests>(configuration);
|
||||
}
|
||||
|
||||
[IterationSetup]
|
||||
public void GenerateReportData()
|
||||
{
|
||||
ImagePlaceholder.Solid = true;
|
||||
|
||||
var model = DataSource.GetReport();
|
||||
var report = new StandardReport(model);
|
||||
Metadata = report.GetMetadata();
|
||||
|
||||
var documentContainer = new DocumentContainer();
|
||||
report.Compose(documentContainer);
|
||||
Content = documentContainer.Compose();
|
||||
|
||||
PageContext = new PageContext();
|
||||
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content);
|
||||
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
Content.VisitChildren(x =>
|
||||
{
|
||||
if (x is ICacheable)
|
||||
x.CreateProxy(y => new CacheProxy(y));
|
||||
});
|
||||
|
||||
sw.Stop();
|
||||
Console.WriteLine($"Creating cache took: {sw.ElapsedMilliseconds}");
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void GenerationTest()
|
||||
{
|
||||
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer;
|
||||
using QuestPDF.ReportSample.Layouts;
|
||||
|
||||
namespace QuestPDF.ReportSample
|
||||
{
|
||||
public class ReportGeneration
|
||||
{
|
||||
private StandardReport Report { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var model = DataSource.GetReport();
|
||||
Report = new StandardReport(model);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateAndShowPdf()
|
||||
{
|
||||
Report.ShowInPreviewer();
|
||||
|
||||
//ImagePlaceholder.Solid = true;
|
||||
//
|
||||
// var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf");
|
||||
// Report.GeneratePdf(path);
|
||||
// Process.Start("explorer.exe", path);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateAndShowXps()
|
||||
{
|
||||
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.xps");
|
||||
Report.GenerateXps(path);
|
||||
Process.Start("explorer.exe", path);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Profile()
|
||||
{
|
||||
ImagePlaceholder.Solid = true;
|
||||
|
||||
var container = new DocumentContainer();
|
||||
Report.Compose(container);
|
||||
var content = container.Compose();
|
||||
|
||||
var pageContext = new PageContext();
|
||||
|
||||
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content);
|
||||
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.UnitTests.TestEngine;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ImageTests
|
||||
{
|
||||
[Test]
|
||||
public void Measure_TakesAvailableSpaceRegardlessOfSize()
|
||||
{
|
||||
TestPlan
|
||||
.For(x => new Image
|
||||
{
|
||||
InternalImage = GenerateImage(400, 300)
|
||||
})
|
||||
.MeasureElement(new Size(300, 200))
|
||||
.CheckMeasureResult(SpacePlan.FullRender(300, 200));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Draw_TakesAvailableSpaceRegardlessOfSize()
|
||||
{
|
||||
TestPlan
|
||||
.For(x => new Image
|
||||
{
|
||||
InternalImage = GenerateImage(400, 300)
|
||||
})
|
||||
.DrawElement(new Size(300, 200))
|
||||
.ExpectCanvasDrawImage(new Position(0, 0), new Size(300, 200))
|
||||
.CheckDrawResult();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fluent_RecognizesImageProportions()
|
||||
{
|
||||
var image = GenerateImage(600, 200).Encode(SKEncodedImageFormat.Png, 100).ToArray();
|
||||
|
||||
TestPlan
|
||||
.For(x =>
|
||||
{
|
||||
var container = new Container();
|
||||
container.Image(image);
|
||||
return container;
|
||||
})
|
||||
.MeasureElement(new Size(300, 200))
|
||||
.CheckMeasureResult(SpacePlan.FullRender(300, 100));;
|
||||
}
|
||||
|
||||
SKImage GenerateImage(int width, int height)
|
||||
{
|
||||
var imageInfo = new SKImageInfo(width, height);
|
||||
using var surface = SKSurface.Create(imageInfo);
|
||||
return surface.Snapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer;
|
||||
|
||||
namespace QuestPDF.UnitTests
|
||||
{
|
||||
public class PreviewerTests
|
||||
{
|
||||
[Test]
|
||||
public void SendError()
|
||||
{
|
||||
Document
|
||||
.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header()
|
||||
.Text("Hello PDF!")
|
||||
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(20);
|
||||
|
||||
x.Item().Text(Placeholders.LoremIpsum());
|
||||
x.Item().Image(Placeholders.Image(200, 100));
|
||||
});
|
||||
|
||||
page.Footer()
|
||||
.AlignCenter()
|
||||
.Text(x =>
|
||||
{
|
||||
//throw new Exception("Dupa!");
|
||||
x.Span("Page ");
|
||||
x.CurrentPageNumber();
|
||||
});
|
||||
});
|
||||
})
|
||||
.ShowInPreviewer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.8.0" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using QuestPDF.Previewer;
|
||||
|
||||
namespace QuestPDF.Drawing.Exceptions
|
||||
{
|
||||
public class DocumentLayoutException : Exception
|
||||
{
|
||||
internal LayoutErrorTrace? ElementTrace { get; }
|
||||
|
||||
internal DocumentLayoutException(string message, LayoutErrorTrace? elementTrace = null) : base(message)
|
||||
{
|
||||
ElementTrace = elementTrace;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Drawing.Proxy
|
||||
{
|
||||
internal class DebuggingProxy : ElementProxy
|
||||
{
|
||||
internal List<MeasureDetails> Measurements { get; } = new();
|
||||
|
||||
public DebuggingProxy(Element child)
|
||||
{
|
||||
Child = child;
|
||||
}
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
var spacePlan = Child.Measure(availableSpace);
|
||||
Measurements.Add(new MeasureDetails(availableSpace, spacePlan));
|
||||
return spacePlan;
|
||||
}
|
||||
}
|
||||
|
||||
internal struct MeasureDetails
|
||||
{
|
||||
public Size AvailableSpace { get; }
|
||||
public SpacePlan SpacePlan { get;}
|
||||
|
||||
public MeasureDetails(Size availableSpace, SpacePlan spacePlan)
|
||||
{
|
||||
AvailableSpace = availableSpace;
|
||||
SpacePlan = spacePlan;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer;
|
||||
|
||||
namespace QuestPDF.Drawing.Proxy;
|
||||
|
||||
internal static class Helpers
|
||||
{
|
||||
public static IReadOnlyCollection<DocumentElementProperty> GetElementConfiguration(this IElement element)
|
||||
{
|
||||
return element
|
||||
.GetType()
|
||||
.GetProperties()
|
||||
.Select(x => new
|
||||
{
|
||||
Property = x.Name.PrettifyName(),
|
||||
Value = x.GetValue(element)
|
||||
})
|
||||
.Where(x => !(x.Value is IElement))
|
||||
.Where(x => x.Value is string || !(x.Value is IEnumerable))
|
||||
.Where(x => !(x.Value is TextStyle))
|
||||
.Select(x => new DocumentElementProperty
|
||||
{
|
||||
Label = x.Property,
|
||||
Value = FormatValue(x.Value)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
string FormatValue(object value)
|
||||
{
|
||||
const int maxLength = 100;
|
||||
|
||||
var text = value?.ToString() ?? "-";
|
||||
|
||||
if (text.Length < maxLength)
|
||||
return text;
|
||||
|
||||
return text.AsSpan(0, maxLength).ToString() + "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer.Inspection;
|
||||
|
||||
namespace QuestPDF.Drawing.Proxy
|
||||
{
|
||||
internal class InspectionProxy : ElementProxy
|
||||
{
|
||||
public Dictionary<int, InspectionStateItem> Statistics { get; set; } = new();
|
||||
|
||||
public InspectionProxy(Element child)
|
||||
{
|
||||
Child = child;
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
if (Canvas is SkiaCanvasBase canvas)
|
||||
{
|
||||
var matrix = canvas.Canvas.TotalMatrix;
|
||||
|
||||
var inspectionItem = new InspectionStateItem
|
||||
{
|
||||
Element = Child,
|
||||
Position = new Position(matrix.TransX, matrix.TransY),
|
||||
Size = availableSpace
|
||||
};
|
||||
|
||||
Statistics[PageContext.CurrentPage] = inspectionItem;
|
||||
}
|
||||
|
||||
base.Draw(availableSpace);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using System.Linq;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer;
|
||||
|
||||
namespace QuestPDF.Drawing.Proxy
|
||||
{
|
||||
internal static class LayoutErrorAnalyzer
|
||||
{
|
||||
public static void ApplyLayoutDebugging(this Container content)
|
||||
{
|
||||
content.VisitChildren(x =>
|
||||
{
|
||||
x.CreateProxy(y => y is ElementProxy ? y : new DebuggingProxy(y));
|
||||
});
|
||||
}
|
||||
|
||||
public static void RemoveExistingProxies(this Container content)
|
||||
{
|
||||
content.VisitChildren(x =>
|
||||
{
|
||||
x.CreateProxy(y => y is ElementProxy proxy ? proxy.Child : y);
|
||||
});
|
||||
}
|
||||
|
||||
public static void RemoveExistingContainers(this Container content)
|
||||
{
|
||||
content.VisitChildren(x =>
|
||||
{
|
||||
x.CreateProxy(y => y is Container container ? container.Child : y);
|
||||
});
|
||||
}
|
||||
|
||||
public static LayoutErrorTrace CollectLayoutErrorTrace(this Container content)
|
||||
{
|
||||
var proxies = content.ExtractProxyOfType<DebuggingProxy>();
|
||||
return Map(proxies);
|
||||
|
||||
LayoutErrorTrace Map(TreeNode<DebuggingProxy> proxy)
|
||||
{
|
||||
var element = proxy.Value.Child;
|
||||
|
||||
return new LayoutErrorTrace
|
||||
{
|
||||
ElementType = element.GetType().Name,
|
||||
IsSingleChildContainer = element is ContainerElement,
|
||||
ElementProperties = element.GetElementConfiguration().ToList(),
|
||||
|
||||
Measurements = proxy
|
||||
.Value
|
||||
.Measurements
|
||||
.Select(x => new LayoutErrorMeasurement
|
||||
{
|
||||
AvailableSpace = new QuestPDF.Previewer.Size()
|
||||
{
|
||||
Width = x.AvailableSpace.Width,
|
||||
Height = x.AvailableSpace.Height
|
||||
},
|
||||
SpacePlan = new QuestPDF.Previewer.SpacePlan()
|
||||
{
|
||||
Width = x.SpacePlan.Width,
|
||||
Height = x.SpacePlan.Height,
|
||||
Type = x.SpacePlan.Type
|
||||
}
|
||||
})
|
||||
.ToList(),
|
||||
|
||||
Children = proxy.Children.Select(Map).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
internal class DynamicImage : Element
|
||||
{
|
||||
public Func<Size, byte[]>? Source { get; set; }
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
return availableSpace.IsNegative()
|
||||
? SpacePlan.Wrap()
|
||||
: SpacePlan.FullRender(availableSpace);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
var imageData = Source?.Invoke(availableSpace);
|
||||
|
||||
if (imageData == null)
|
||||
return;
|
||||
|
||||
using var image = SKImage.FromEncodedData(imageData);
|
||||
Canvas.DrawImage(image, Position.Zero, availableSpace);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
internal class Image : Element, ICacheable
|
||||
{
|
||||
public SKImage? InternalImage { get; set; }
|
||||
|
||||
~Image()
|
||||
{
|
||||
InternalImage?.Dispose();
|
||||
}
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
return availableSpace.IsNegative()
|
||||
? SpacePlan.Wrap()
|
||||
: SpacePlan.FullRender(availableSpace);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
if (InternalImage == null)
|
||||
return;
|
||||
|
||||
Canvas.DrawImage(InternalImage, Position.Zero, availableSpace);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
public static class ImageExtensions
|
||||
{
|
||||
public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(imageData);
|
||||
parent.Image(image, scaling);
|
||||
}
|
||||
|
||||
public static void Image(this IContainer parent, string filePath, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(filePath);
|
||||
parent.Image(image, scaling);
|
||||
}
|
||||
|
||||
public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(fileStream);
|
||||
parent.Image(image, scaling);
|
||||
}
|
||||
|
||||
private static void Image(this IContainer parent, SKImage image, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
{
|
||||
if (image == null)
|
||||
throw new DocumentComposeException("Cannot load or decode provided image.");
|
||||
|
||||
var aspectRatio = image.Width / (float)image.Height;
|
||||
|
||||
var imageElement = new Image
|
||||
{
|
||||
InternalImage = image
|
||||
};
|
||||
|
||||
if (scaling != ImageScaling.Resize)
|
||||
parent = parent.AspectRatio(aspectRatio, Map(scaling));
|
||||
|
||||
parent.Element(imageElement);
|
||||
|
||||
static AspectRatioOption Map(ImageScaling scaling)
|
||||
{
|
||||
return scaling switch
|
||||
{
|
||||
ImageScaling.FitWidth => AspectRatioOption.FitWidth,
|
||||
ImageScaling.FitHeight => AspectRatioOption.FitHeight,
|
||||
ImageScaling.FitArea => AspectRatioOption.FitArea,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static void Image(this IContainer element, Func<Size, byte[]> imageSource)
|
||||
{
|
||||
element.Element(new DynamicImage
|
||||
{
|
||||
Source = imageSource
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using QuestPDF.Drawing;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public interface IDocument
|
||||
{
|
||||
DocumentMetadata GetMetadata();
|
||||
void Compose(IDocumentContainer container);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Previewer.Inspection;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
internal sealed class IncompatibleVersionApiRequest
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal sealed class NotifyPresenceApiRequest
|
||||
{
|
||||
public string ClientId { get; set; }
|
||||
public string LibraryVersion { get; set; }
|
||||
public bool IsDotnet6OrBeyond { get; set; }
|
||||
public bool IsDotnet3OrBeyond { get; set; }
|
||||
public bool IsExecutedInUnitTest { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class DocumentPreviewApiRequest
|
||||
{
|
||||
public ICollection<PageSnapshot> PageSnapshots { get; set; }
|
||||
public InspectionElement DocumentHierarchy { get; set; }
|
||||
|
||||
public class PageSnapshot
|
||||
{
|
||||
public string ResourceId { get; } = Guid.NewGuid().ToString("N");
|
||||
public int PageNumber { get; set; }
|
||||
|
||||
public float Width { get; set; }
|
||||
public float Height { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GenericErrorApiRequest
|
||||
{
|
||||
public GenericError? Error { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class LayoutErrorApiRequest
|
||||
{
|
||||
public LayoutErrorTrace? Trace { get; set; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#if NETCOREAPP3_0_OR_GREATER
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
internal static class Helpers
|
||||
{
|
||||
public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value, options);
|
||||
var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
return client.PostAsync(requestUri, jsonContent, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,60 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuestPDF.Drawing.Proxy;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Previewer.Inspection;
|
||||
|
||||
public static class DocumentHierarchyProcessor
|
||||
{
|
||||
internal static InspectionElement ExtractDocumentHierarchy(this Container content)
|
||||
{
|
||||
content.RemoveExistingContainers();
|
||||
|
||||
var proxies = content.ExtractProxyOfType<InspectionProxy>();
|
||||
return Map(proxies);
|
||||
|
||||
InspectionElement Map(TreeNode<InspectionProxy> treeNode)
|
||||
{
|
||||
var proxy = treeNode.Value;
|
||||
var element = proxy.Child;
|
||||
|
||||
var locations = proxy
|
||||
.Statistics
|
||||
.Keys
|
||||
.Select(x =>
|
||||
{
|
||||
var statistics = proxy.Statistics[x];
|
||||
|
||||
return new InspectionElementLocation
|
||||
{
|
||||
PageNumber = x,
|
||||
Position = new MyPosition
|
||||
{
|
||||
Top = statistics.Position.Y,
|
||||
Left = statistics.Position.X
|
||||
},
|
||||
Size = new Size
|
||||
{
|
||||
Width = statistics.Size.Width,
|
||||
Height = statistics.Size.Height
|
||||
}
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new InspectionElement
|
||||
{
|
||||
ElementType = element.GetType().Name,
|
||||
IsSingleChildContainer = element is ContainerElement,
|
||||
Location = locations,
|
||||
Properties = element.GetElementConfiguration().ToList(),
|
||||
Children = treeNode.Children.Select(Map).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Drawing;
|
||||
|
||||
namespace QuestPDF.Previewer.Inspection;
|
||||
|
||||
internal class DocumentPreviewResult
|
||||
{
|
||||
public IList<PreviewerPicture> PageSnapshots { get; set; }
|
||||
public InspectionElement DocumentHierarchy { get; set; }
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Previewer.Inspection
|
||||
{
|
||||
internal class InspectionStateItem
|
||||
{
|
||||
public Element Element { get; internal set; }
|
||||
public Infrastructure.Size Size { get; internal set; }
|
||||
public Position Position { get; internal set; }
|
||||
}
|
||||
|
||||
internal class MyPosition
|
||||
{
|
||||
public float Top { get; set; }
|
||||
public float Left { get; set; }
|
||||
}
|
||||
|
||||
internal class InspectionElementLocation
|
||||
{
|
||||
public int PageNumber { get; internal set; }
|
||||
public MyPosition Position { get; internal set; }
|
||||
public Size Size { get; internal set; }
|
||||
}
|
||||
|
||||
internal class InspectionElement
|
||||
{
|
||||
public string ElementType { get; set; }
|
||||
public bool IsSingleChildContainer { get; set; }
|
||||
public IReadOnlyCollection<InspectionElementLocation> Location { get; set; }
|
||||
public IReadOnlyCollection<DocumentElementProperty> Properties { get; set; }
|
||||
public ICollection<InspectionElement> Children { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer.Inspection;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
internal sealed class DocumentElementProperty
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class Size
|
||||
{
|
||||
public float Width { get; set; }
|
||||
public float Height { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class SpacePlan
|
||||
{
|
||||
public float Width { get; set; }
|
||||
public float Height { get; set; }
|
||||
public SpacePlanType Type { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal sealed class LayoutErrorTrace
|
||||
{
|
||||
public string ElementType { get; set; }
|
||||
public bool IsSingleChildContainer { get; set; }
|
||||
public IReadOnlyCollection<DocumentElementProperty> ElementProperties { get; set; }
|
||||
public IReadOnlyCollection<LayoutErrorMeasurement> Measurements { get; set; }
|
||||
public IReadOnlyCollection<LayoutErrorTrace> Children { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class LayoutErrorMeasurement
|
||||
{
|
||||
public Size AvailableSpace { get; set; }
|
||||
public SpacePlan SpacePlan { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
internal sealed class GenericError
|
||||
{
|
||||
public string ExceptionType { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string StackTrace { get; set; }
|
||||
public GenericError? InnerException { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#if NETCOREAPP3_0_OR_GREATER
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static void ShowInPreviewer(this IDocument document, int port = 12500)
|
||||
{
|
||||
document.ShowInPreviewerAsync(port).ConfigureAwait(true).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var previewerService = new PreviewerService(port);
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
previewerService.OnLostConnection += () => cancellationTokenSource.Cancel();
|
||||
|
||||
await previewerService.Connect();
|
||||
await RefreshPreview();
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
|
||||
#endif
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// ReSharper disable once MethodSupportsCancellation
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(1000));
|
||||
}
|
||||
|
||||
Task RefreshPreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
var documentPreviewResult = DocumentGenerator.GeneratePreviewerPictures(document);
|
||||
return previewerService.ShowDocumentPreview(documentPreviewResult);
|
||||
}
|
||||
catch (DocumentLayoutException exception)
|
||||
{
|
||||
return previewerService.ShowLayoutError(exception);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return previewerService.ShowGenericError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,267 +0,0 @@
|
||||
#if NETCOREAPP3_0_OR_GREATER
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Previewer.Inspection;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
internal class PreviewerService
|
||||
{
|
||||
private string ClientId { get; } = Guid.NewGuid().ToString("D");
|
||||
private string LibraryVersion { get; } = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString();
|
||||
|
||||
private int Port { get; }
|
||||
private HttpClient HttpClient { get; }
|
||||
|
||||
public event Action? OnLostConnection;
|
||||
|
||||
private const int RequiredPreviewerVersionMajor = 2023;
|
||||
private const int RequiredPreviewerVersionMinor = 1;
|
||||
|
||||
public PreviewerService(int port)
|
||||
{
|
||||
Port = port;
|
||||
HttpClient = new()
|
||||
{
|
||||
BaseAddress = new Uri($"http://localhost:{port}/"),
|
||||
Timeout = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
}
|
||||
|
||||
internal static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
MaxDepth = 256,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
|
||||
}
|
||||
};
|
||||
|
||||
#region Connection
|
||||
|
||||
public async Task Connect()
|
||||
{
|
||||
var isAvailable = await IsPreviewerAvailable();
|
||||
|
||||
if (!isAvailable)
|
||||
{
|
||||
StartPreviewer();
|
||||
await WaitForConnection();
|
||||
}
|
||||
|
||||
var previewerVersion = await GetPreviewerVersion();
|
||||
await CheckVersionCompatibility(previewerVersion);
|
||||
|
||||
StartNotifyPresenceTask();
|
||||
}
|
||||
|
||||
private async Task<bool> IsPreviewerAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var result = await HttpClient.GetAsync("/ping");
|
||||
return result.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartPreviewer()
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = "questpdf-previewer",
|
||||
Arguments = $"{Port}",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new Exception("Cannot start the QuestPDF Previewer tool. " +
|
||||
"Please install it by executing in terminal the following command: 'dotnet tool install --global QuestPDF.Previewer'.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitForConnection()
|
||||
{
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// ReSharper disable once MethodSupportsCancellation
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(250));
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
throw new Exception($"Cannot connect to the QuestPDF Previewer tool. Please make sure that your Operating System does not block HTTP connections on port {Port}.");
|
||||
|
||||
var isConnected = await IsPreviewerAvailable();
|
||||
|
||||
if (isConnected)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checking compatibility
|
||||
|
||||
private async Task<Version> GetPreviewerVersion()
|
||||
{
|
||||
using var result = await HttpClient.GetAsync("/version");
|
||||
var response = await result.Content.ReadFromJsonAsync<string>();
|
||||
return Version.Parse(response);
|
||||
}
|
||||
|
||||
private async Task CheckVersionCompatibility(Version version)
|
||||
{
|
||||
if (version is { Major: RequiredPreviewerVersionMajor, Minor: RequiredPreviewerVersionMinor })
|
||||
return;
|
||||
|
||||
await ShowIncompatibleVersion();
|
||||
|
||||
throw new Exception($"Previewer version is not compatible. Possible solutions: " +
|
||||
$"1) Update the QuestPDF library to newer version. " +
|
||||
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version {RequiredPreviewerVersionMajor}.{RequiredPreviewerVersionMinor}'");
|
||||
}
|
||||
|
||||
private async Task ShowIncompatibleVersion()
|
||||
{
|
||||
var payload = new IncompatibleVersionApiRequest();
|
||||
await HttpClient.PostAsJsonAsync("/update/incompatibleVersion", payload, JsonSerializerOptions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reporting presence
|
||||
|
||||
private void StartNotifyPresenceTask()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await NotifyPresence();
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task NotifyPresence()
|
||||
{
|
||||
var payload = new NotifyPresenceApiRequest
|
||||
{
|
||||
ClientId = ClientId,
|
||||
LibraryVersion = LibraryVersion,
|
||||
IsDotnet6OrBeyond = RuntimeDetector.IsNet6OrGreater,
|
||||
IsDotnet3OrBeyond = RuntimeDetector.IsNet3OrGreater,
|
||||
IsExecutedInUnitTest = UnitTestDetector.RunningInUnitTest
|
||||
};
|
||||
|
||||
await HttpClient.PostAsJsonAsync("/update/notifyPresence", payload);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Updating state
|
||||
|
||||
public async Task ShowDocumentPreview(DocumentPreviewResult documentPreviewResult)
|
||||
{
|
||||
using var multipartContent = new MultipartFormDataContent();
|
||||
|
||||
var pages = new List<DocumentPreviewApiRequest.PageSnapshot>();
|
||||
|
||||
foreach (var snapshot in documentPreviewResult.PageSnapshots)
|
||||
{
|
||||
var page = new DocumentPreviewApiRequest.PageSnapshot
|
||||
{
|
||||
PageNumber = documentPreviewResult.PageSnapshots.IndexOf(snapshot) + 1,
|
||||
Width = snapshot.Size.Width,
|
||||
Height = snapshot.Size.Height
|
||||
};
|
||||
|
||||
pages.Add(page);
|
||||
|
||||
var pictureStream = snapshot.Picture.Serialize().AsStream();
|
||||
multipartContent.Add(new StreamContent(pictureStream), "snapshots", page.ResourceId);
|
||||
}
|
||||
|
||||
var request = new DocumentPreviewApiRequest
|
||||
{
|
||||
PageSnapshots = pages,
|
||||
DocumentHierarchy = documentPreviewResult.DocumentHierarchy
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(request, JsonSerializerOptions);
|
||||
var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
multipartContent.Add(jsonContent, "request");
|
||||
|
||||
using var response = await HttpClient.PostAsync("/update/documentPreview", multipartContent);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
foreach (var picture in documentPreviewResult.PageSnapshots)
|
||||
picture.Picture.Dispose();
|
||||
}
|
||||
|
||||
public async Task ShowGenericError(Exception exception)
|
||||
{
|
||||
var payload = new GenericErrorApiRequest
|
||||
{
|
||||
Error = MapException(exception)
|
||||
};
|
||||
|
||||
await HttpClient.PostAsJsonAsync("/update/genericError", payload, JsonSerializerOptions);
|
||||
|
||||
static GenericError? MapException(Exception? exception)
|
||||
{
|
||||
if (exception == null)
|
||||
return null;
|
||||
|
||||
return new GenericError
|
||||
{
|
||||
ExceptionType = exception.GetType().FullName,
|
||||
Message = exception.Message,
|
||||
StackTrace = exception.StackTrace,
|
||||
InnerException = MapException(exception.InnerException)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShowLayoutError(DocumentLayoutException exception)
|
||||
{
|
||||
var payload = new LayoutErrorApiRequest
|
||||
{
|
||||
Trace = exception.ElementTrace
|
||||
};
|
||||
|
||||
await HttpClient.PostAsJsonAsync("/update/layoutError", payload, JsonSerializerOptions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
public static class RuntimeDetector
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
public static bool IsNet6OrGreater => true;
|
||||
#else
|
||||
public static bool IsNet6OrGreater => false;
|
||||
#endif
|
||||
|
||||
#if NETCOREAPP3_0_OR_GREATER
|
||||
public static bool IsNet3OrGreater => true;
|
||||
#else
|
||||
public static bool IsNet6OrGreater => false;
|
||||
#endif
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuestPDF.Drawing.Proxy;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
internal record TreeNode<T>(T Value, ICollection<TreeNode<T>> Children);
|
||||
|
||||
internal static class TreeTraversal
|
||||
{
|
||||
public static TreeNode<T> ExtractProxyOfType<T>(this Element root) where T : ElementProxy
|
||||
{
|
||||
return Traverse(root).FirstOrDefault();
|
||||
|
||||
IEnumerable<TreeNode<T>> Traverse(Element element)
|
||||
{
|
||||
if (element is T proxy)
|
||||
{
|
||||
var result = new TreeNode<T>(proxy, new List<TreeNode<T>>());
|
||||
|
||||
proxy
|
||||
.Child!
|
||||
.GetChildren()
|
||||
.SelectMany(Traverse)
|
||||
.ToList()
|
||||
.ForEach(result.Children.Add);
|
||||
|
||||
yield return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var treeNode in element.GetChildren().SelectMany(Traverse))
|
||||
yield return treeNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
internal static class UnitTestDetector
|
||||
{
|
||||
public static bool RunningInUnitTest { get; }
|
||||
|
||||
private static string[] UnitTestAssemblies = { "xunit", "nunit", "mstest" };
|
||||
|
||||
static UnitTestDetector()
|
||||
{
|
||||
RunningInUnitTest = AppDomain
|
||||
.CurrentDomain
|
||||
.GetAssemblies()
|
||||
.Select(x => x.FullName.ToLowerInvariant())
|
||||
.Any(x => UnitTestAssemblies.Any(x.Contains));
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF</PackageId>
|
||||
<Version>2022.11.0</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>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<LangVersion>10</LangVersion>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>
|
||||
<PackageProjectUrl>https://www.questpdf.com/</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
|
||||
<PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.3" />
|
||||
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
<InternalsVisibleTo Include="QuestPDF.ReportSample" />
|
||||
<InternalsVisibleTo Include="QuestPDF.UnitTests" />
|
||||
<InternalsVisibleTo Include="QuestPDF.Examples" />
|
||||
<InternalsVisibleTo Include="QuestPDF.Previewer" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\ImagePlaceholder.png" />
|
||||
<EmbeddedResource Include="Resources\Logo.png" />
|
||||
<None Remove="ImagePlaceholder.png" />
|
||||
<None Include="Resources\Logo.png">
|
||||
<Pack>true</Pack>
|
||||
<Visible>false</Visible>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Remove="Resources\DefaultFont\Lato-Black.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-Black.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-BlackItalic.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-BlackItalic.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-Bold.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-Bold.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-BoldItalic.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-BoldItalic.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-Italic.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-Italic.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-Light.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-Light.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-LightItalic.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-LightItalic.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-Regular.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-Regular.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-Thin.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-Thin.ttf" />
|
||||
<None Remove="Resources\DefaultFont\Lato-ThinItalic.ttf" />
|
||||
<EmbeddedResource Include="Resources\DefaultFont\Lato-ThinItalic.ttf" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Previewer\Models\GenericError.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,93 +0,0 @@
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://github.com/QuestPDF/QuestPDF/stargazers)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://github.com/QuestPDF/QuestPDF/blob/main/LICENSE)
|
||||
[](https://github.com/sponsors/QuestPDF)
|
||||
|
||||
QuestPDF is an open-source .NET library for PDF documents generation.
|
||||
|
||||
It offers a layout engine designed with a 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 behavior 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. All of it done with predictable and discoverable Fluent API.
|
||||
|
||||
## 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/).
|
||||
|
||||
```xml
|
||||
// Package Manager
|
||||
Install-Package QuestPDF
|
||||
|
||||
// .NET CLI
|
||||
dotnet add package QuestPDF
|
||||
|
||||
// Package reference in .csproj file
|
||||
<PackageReference Include="QuestPDF" Version="2022.11.0" />
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
[](https://www.questpdf.com/documentation/getting-started.html)
|
||||
A short and easy to follow tutorial showing how to design an invoice document under 200 lines of code.
|
||||
|
||||
|
||||
[](https://www.questpdf.com/documentation/api-reference.html)
|
||||
A detailed description of behavior of all available components and how to use them with C# Fluent API.
|
||||
|
||||
|
||||
[](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)
|
||||
Everything that may help you designing great reports and create reusable code that is easy to maintain.
|
||||
|
||||
## Simplicity is the key
|
||||
|
||||
How easy it is to start and prototype with QuestPDF? Really easy thanks to its minimal API! Please analyse the code below:
|
||||
|
||||
```#
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
// code in your main method
|
||||
Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.Background(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header()
|
||||
.Text("Hello PDF!")
|
||||
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(20);
|
||||
|
||||
x.Item().Text(Placeholders.LoremIpsum());
|
||||
x.Item().Image(Placeholders.Image(200, 100));
|
||||
});
|
||||
|
||||
page.Footer()
|
||||
.AlignCenter()
|
||||
.Text(x =>
|
||||
{
|
||||
x.Span("Page ");
|
||||
x.CurrentPageNumber();
|
||||
});
|
||||
});
|
||||
})
|
||||
.GeneratePdf("hello.pdf");
|
||||
```
|
||||
|
||||
And compare it to the produced PDF file:
|
||||
|
||||

|
||||
|
||||
## Are you ready for more?
|
||||
|
||||
The Fluent API of QuestPDF scales really well. It is easy to create and maintain even most complex documents. Read [the Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html) to learn QuestPDF basics and implement an invoice under 200 lines of code. You can also investigate and play with the code from [the example repository](https://github.com/QuestPDF/example-invoice).
|
||||
|
||||

|
||||
@@ -1,3 +0,0 @@
|
||||
Added support for the right-to-left content direction.
|
||||
Fixed: word-wrapping algorithm does not work correctly with right-to-left languages.
|
||||
Fixed: Page.Size() API incorrectly uses Unit.Inch as default unit. Replaced with Unit.Point for consistency.
|
||||
@@ -0,0 +1,109 @@
|
||||
<a href="https://www.questpdf.com/" target="_blank">
|
||||
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/logo.svg" width="400">
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
[](https://www.questpdf.com)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://github.com/QuestPDF/QuestPDF/stargazers)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://www.questpdf.com/pricing.html#license)
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
### QuestPDF is a modern open-source .NET library for PDF document generation. Offering comprehensive layout engine powered by concise and discoverable C# Fluent API.
|
||||
|
||||
<img src="https://github.com/QuestPDF/QuestPDF-Documentation/blob/main/docs/public/previewer/animation.gif?raw=true" width="100%">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>👨💻</td>
|
||||
<td>Design PDF documents using C# and employ a code-only approach. Utilize your version control system to its fullest potential.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🧱</td>
|
||||
<td>Compose PDF document with a range of powerful and predictable structural elements, such as text, image, border, table, and many more.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚙️</td>
|
||||
<td>Utilize a comprehensive layout engine, specifically designed for PDF document generation and paging support.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>📖</td>
|
||||
<td>Write code using concise and easy-to-understand C# Fluent API. Utilize IntelliSense to quickly discover available options.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔗</td>
|
||||
<td>Don't be limited to any proprietary scripting language or format. Follow your experience and leverage all modern C# features.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⌛</td>
|
||||
<td>Save time thanks to a hot-reload capability, allowing real-time PDF document preview without code recompilation.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## Please help by giving a star
|
||||
|
||||
Choosing a project dependency could be difficult. We need to ensure stability and maintainability of our projects. Surveys show that GitHub stars count play an important factor when assessing library quality.
|
||||
|
||||
⭐ Please give this repository a star. It takes seconds and help thousands of developers! ⭐
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/9263853/236193691-efb9cff8-e919-4826-9720-dcfb492980ae.png" width="800" />
|
||||
|
||||
|
||||
## Please share with the community
|
||||
|
||||
As an open-source project without funding, I cannot afford advertising QuestPDF in a typical way. Instead, the library relies on community interactions. Please consider sharing a post about QuestPDF and the value it provides. It really does help!
|
||||
|
||||
[](https://reddit.com/submit?url=https://github.com/QuestPDF/QuestPDF&title=Check%20out%20QuestPDF%20%F0%9F%8E%8A%20a%20modern%20open-source%20.NET%20library%20%20for%20PDF%20document%20generation%20%F0%9F%9A%80)
|
||||
[](https://twitter.com/share?url=https://github.com/QuestPDF/QuestPDF&text=Check%20out%20QuestPDF%20%F0%9F%8E%8A%20a%20modern%20open-source%20.NET%20library%20%20for%20PDF%20document%20generation%20%F0%9F%9A%80%20%23dotnet%20%23csharp%20%23questpdf)
|
||||
[](https://news.ycombinator.com/submitlink?u=https://github.com/QuestPDF/QuestPDF&t=QuestPDF%20-%20a%20modern%20open-source%20.NET%20library%20%20for%20PDF%20document%20generation)
|
||||
[](https://www.facebook.com/sharer/sharer.php?u=https://github.com/QuestPDF/QuestPDF)
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## Let's get started
|
||||
|
||||
Begin exploring the QuestPDF library today. You are 250 lines of C# code away from creating a fully functional PDF invoice implementation.
|
||||
|
||||
Read the Getting Started tutorial to familiarize yourself with general library architecture, important layout structures as well as to better understand helpful patterns and practices. Easily start designing your PDF documents, reports, invoices and even more.
|
||||
|
||||
[](https://www.questpdf.com/getting-started.html)
|
||||
|
||||
<img src="https://github.com/QuestPDF/QuestPDF-Documentation/blob/main/docs/public/invoice-small.png?raw=true" width="400px">
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## Library License
|
||||
|
||||
If you are consuming the QuestPDF library as a Direct Package Dependency for usage in a Closed Source software in the capacity of a for-profit company/individual with more than 1M USD annual gross revenue, you must purchase the `QuestPDF Professional or Enterprise` License, depending on number of software developers.
|
||||
|
||||
We love and highly appreciate the .NET Community and therefore the vast majority of users are welcome to use the library completely for free. If you do not meet the criteria described above, you are eligible to use the `QuestPDF Community MIT` License, without any limitations, even for commercial usage.
|
||||
|
||||
If you still want to support library development, please consider purchasing the Professional License.
|
||||
|
||||
[](https://www.questpdf.com/pricing.html#license)
|
||||
[](https://www.questpdf.com/pricing.html#tiers)
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## QuestPDF on JetBrains OSS Power-Ups
|
||||
|
||||
QuestPDF was presented on one of the episodes of OSS Power-Ups hosted by JetBrains. Huge thanks for Matthias Koch and entire JetBrains team for giving me a chance to show QuestPDF. You are the best!
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=-iYvZvpLX0g">
|
||||
<img src="https://github.com/QuestPDF/QuestPDF-Documentation/blob/main/docs/public/jetbrains-oss-powerups-youtube.png?raw=true" width="600px">
|
||||
</a>
|
||||
|
||||
[](https://www.youtube.com/watch?v=-iYvZvpLX0g)
|
||||
@@ -1 +0,0 @@
|
||||
<svg height="180" viewBox="0 0 180 180" width="180" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="32.64" x2="82.77" y1="61.16" y2="85.54"><stop offset=".21" stop-color="#fe2857"/><stop offset="1" stop-color="#293896"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="17.38" x2="82.95" y1="69.86" y2="21.23"><stop offset="0" stop-color="#fe2857"/><stop offset=".01" stop-color="#fe2857"/><stop offset=".86" stop-color="#ff318c"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="74.17" x2="160.27" y1="21.58" y2="99.76"><stop offset=".02" stop-color="#ff318c"/><stop offset=".21" stop-color="#fe2857"/><stop offset=".86" stop-color="#fdb60d"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="155.46" x2="55.07" y1="89.8" y2="158.9"><stop offset=".01" stop-color="#fdb60d"/><stop offset=".86" stop-color="#fcf84a"/></linearGradient><path d="m81.56 83.71-41.35-35a15 15 0 1 0 -14.47 25.7h.15l.39.12 52.16 15.89a3.53 3.53 0 0 0 1.18.21 3.73 3.73 0 0 0 1.93-6.91z" fill="url(#a)"/><path d="m89.85 25.93a10.89 10.89 0 0 0 -16.85-9.18l-50.5 30.66a15 15 0 1 0 17.9 24l45.27-36.89.36-.3a10.93 10.93 0 0 0 3.82-8.29z" fill="url(#b)"/><path d="m163.29 92-76.62-73.79a10.91 10.91 0 1 0 -14.81 16l.14.12 81.4 68.58a7.36 7.36 0 0 0 12.09-5.65 7.39 7.39 0 0 0 -2.2-5.26z" fill="url(#c)"/><path d="m165.5 97.29a7.35 7.35 0 0 0 -11.67-6l-92.71 45.3a15 15 0 1 0 15.48 25.59l85.73-58.84a7.35 7.35 0 0 0 3.17-6.05z" fill="url(#d)"/><path d="m60 60h60v60h-60z"/><g fill="#fff"><path d="m66.53 108.75h22.5v3.75h-22.5z"/><path d="m65.59 75.47 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.45 1.06-1.32v-5.92h2.58v5.94a3.44 3.44 0 0 1 -.92 2.63 3.52 3.52 0 0 1 -2.57 1 3.84 3.84 0 0 1 -3.29-1.62z"/><path d="m73.53 67.52h7.53v2.19h-5v1.43h4.49v2h-4.45v1.49h5v2.2h-7.6z"/><path d="m84.73 69.79h-2.8v-2.27h8.21v2.27h-2.81v7.09h-2.6z"/><path d="m66.63 80.58h4.42a3.47 3.47 0 0 1 2.55.83 2.09 2.09 0 0 1 .61 1.52 2.18 2.18 0 0 1 -1.45 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.69-1.31 2.69-3.55 2.69h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.25c.78 0 1.24-.27 1.24-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86-.02-.53-.4-.87-1.27-.87z"/><path d="m75.45 80.58h4.15a4.14 4.14 0 0 1 3.05 1 2.92 2.92 0 0 1 .83 2.18 3 3 0 0 1 -1.93 2.89l2.24 3.35h-3l-1.89-2.84h-.87v2.84h-2.6zm4 4.5c.87 0 1.4-.43 1.4-1.12 0-.75-.55-1.13-1.41-1.13h-1.39v2.27z"/><path d="m87.09 80.51h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.74h-2.71zm2.28 5.73-1.05-2.65-1.06 2.65z"/><path d="m94 80.55h2.6v9.37h-2.6z"/><path d="m97.56 80.55h2.44l3.37 5v-5h2.57v9.37h-2.27l-3.53-5.14v5.14h-2.58z"/><path d="m106.37 88.53 1.44-1.73a4.86 4.86 0 0 0 3 1.13c.71 0 1.08-.25 1.08-.65 0-.41-.3-.61-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.38-3 3.63-3a5.88 5.88 0 0 1 3.85 1.25l-1.25 1.78a4.56 4.56 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .43.32.62 1.63.91 2.15.47 3.48 1.17 3.48 2.92 0 1.91-1.51 3-3.78 3a6.56 6.56 0 0 1 -4.4-1.45z"/></g><path d="m0 0h180v180h-180z" fill="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,56 +0,0 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer;
|
||||
|
||||
QuestPDF.Settings.EnableCaching = true;
|
||||
QuestPDF.Settings.EnableDebugging = true;
|
||||
|
||||
Document
|
||||
.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header()
|
||||
.Text("Hello PDF!")
|
||||
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(20);
|
||||
|
||||
x.Item().Text(Placeholders.LoremIpsum());
|
||||
x.Item().Image(Placeholders.Image(200, 100));
|
||||
|
||||
foreach (var i in Enumerable.Range(0, 50))
|
||||
{
|
||||
x.Item()
|
||||
.Height(80).Width(100 + i * 5)
|
||||
.Background(Colors.Grey.Lighten3)
|
||||
.AlignCenter().AlignMiddle()
|
||||
.Text(i);
|
||||
}
|
||||
|
||||
//x.Item().Width(1000);
|
||||
});
|
||||
|
||||
page.Footer()
|
||||
.AlignCenter()
|
||||
.Text(x =>
|
||||
{
|
||||
x.Span("Page ");
|
||||
x.CurrentPageNumber();
|
||||
});
|
||||
});
|
||||
})
|
||||
.ShowInPreviewer();
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
public class AlignmentExamples
|
||||
{
|
||||
[Test]
|
||||
public void Example()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(400, 200)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(25)
|
||||
.Border(1)
|
||||
.AlignBottom()
|
||||
.Background(Colors.Grey.Lighten1)
|
||||
.Text("Test");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Examples.Engine;
|
||||
@@ -43,7 +44,7 @@ namespace QuestPDF.Examples
|
||||
.Height(50)
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Text(i)
|
||||
.Text(i.ToString(CultureInfo.InvariantCulture))
|
||||
.FontSize(16 + i / 4);
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Elements;
|
||||
@@ -49,15 +50,15 @@ namespace QuestPDF.Examples
|
||||
{
|
||||
container.MinimalBox().Decoration(decoration =>
|
||||
{
|
||||
decoration.Header().Element(header);
|
||||
decoration.Before().Element(header);
|
||||
|
||||
decoration.Content().Box().Stack(stack =>
|
||||
decoration.Content().Column(column =>
|
||||
{
|
||||
foreach (var row in rows)
|
||||
stack.Item().Element(row.Element);
|
||||
column.Item().Element(row.Element);
|
||||
});
|
||||
|
||||
decoration.Footer().Element(footer);
|
||||
decoration.After().Element(footer);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,15 +83,14 @@ namespace QuestPDF.Examples
|
||||
.BorderBottom(1)
|
||||
.BorderColor(Colors.Grey.Darken2)
|
||||
.Padding(5)
|
||||
.DefaultTextStyle(TextStyle.Default.SemiBold())
|
||||
.Row(row =>
|
||||
{
|
||||
var textStyle = TextStyle.Default.SemiBold();
|
||||
|
||||
row.ConstantItem(30).Text("#", textStyle);
|
||||
row.RelativeItem().Text("Item name", textStyle);
|
||||
row.ConstantItem(50).AlignRight().Text("Count", textStyle);
|
||||
row.ConstantItem(50).AlignRight().Text("Price", textStyle);
|
||||
row.ConstantItem(50).AlignRight().Text("Total", textStyle);
|
||||
row.ConstantItem(30).Text("#");
|
||||
row.RelativeItem().Text("Item name");
|
||||
row.ConstantItem(50).AlignRight().Text("Count");
|
||||
row.ConstantItem(50).AlignRight().Text("Price");
|
||||
row.ConstantItem(50).AlignRight().Text("Total");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -105,7 +105,8 @@ namespace QuestPDF.Examples
|
||||
.Width(context.AvailableSize.Width)
|
||||
.Padding(5)
|
||||
.AlignRight()
|
||||
.Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold());
|
||||
.DefaultTextStyle(TextStyle.Default.FontSize(14).SemiBold())
|
||||
.Text($"Subtotal: {total}$");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,9 +127,9 @@ namespace QuestPDF.Examples
|
||||
.Padding(5)
|
||||
.Row(row =>
|
||||
{
|
||||
row.ConstantItem(30).Text(index + 1);
|
||||
row.ConstantItem(30).Text((index + 1).ToString(CultureInfo.InvariantCulture));
|
||||
row.RelativeItem().Text(item.ItemName);
|
||||
row.ConstantItem(50).AlignRight().Text(item.Count);
|
||||
row.ConstantItem(50).AlignRight().Text(item.Count.ToString(CultureInfo.InvariantCulture));
|
||||
row.ConstantItem(50).AlignRight().Text($"{item.Price}$");
|
||||
row.ConstantItem(50).AlignRight().Text($"{item.Count*item.Price}$");
|
||||
});
|
||||
@@ -164,7 +165,7 @@ namespace QuestPDF.Examples
|
||||
.Decoration(decoration =>
|
||||
{
|
||||
decoration
|
||||
.Header()
|
||||
.Before()
|
||||
.PaddingBottom(5)
|
||||
.Text(text =>
|
||||
{
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Elements;
|
||||
@@ -96,9 +97,9 @@ namespace QuestPDF.Examples
|
||||
{
|
||||
var item = Items[index];
|
||||
|
||||
table.Cell().Element(Style).Text(index + 1);
|
||||
table.Cell().Element(Style).Text((index + 1).ToString(CultureInfo.InvariantCulture));
|
||||
table.Cell().Element(Style).Text(item.ItemName);
|
||||
table.Cell().Element(Style).AlignRight().Text(item.Count);
|
||||
table.Cell().Element(Style).AlignRight().Text(item.Count.ToString(CultureInfo.InvariantCulture));
|
||||
table.Cell().Element(Style).AlignRight().Text($"{item.Price}$");
|
||||
table.Cell().Element(Style).AlignRight().Text($"{item.Count*item.Price}$");
|
||||
|
||||
@@ -13,25 +13,22 @@ namespace QuestPDF.Examples.Engine
|
||||
|
||||
private Action<IDocumentContainer> Content { get; }
|
||||
private int MaxPages { get; }
|
||||
private bool ApplyCaching { get; }
|
||||
private bool ApplyDebugging { get; }
|
||||
|
||||
public SimpleDocument(Action<IDocumentContainer> content, int maxPages, bool applyCaching, bool applyDebugging)
|
||||
{
|
||||
Content = content;
|
||||
MaxPages = maxPages;
|
||||
ApplyCaching = applyCaching;
|
||||
ApplyDebugging = applyDebugging;
|
||||
|
||||
QuestPDF.Settings.EnableCaching = applyCaching;
|
||||
QuestPDF.Settings.EnableDebugging = applyDebugging;
|
||||
QuestPDF.Settings.DocumentLayoutExceptionThreshold = MaxPages;
|
||||
}
|
||||
|
||||
public DocumentMetadata GetMetadata()
|
||||
public DocumentSettings GetSettings()
|
||||
{
|
||||
return new DocumentMetadata()
|
||||
return new DocumentSettings()
|
||||
{
|
||||
RasterDpi = PageSizes.PointsPerInch * ImageScalingFactor,
|
||||
DocumentLayoutExceptionThreshold = MaxPages,
|
||||
ApplyCaching = ApplyCaching,
|
||||
ApplyDebugging = ApplyDebugging
|
||||
ImageRasterDpi = PageSizes.PointsPerInch * ImageScalingFactor
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
@@ -50,6 +51,64 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ScalingImageWithAlpha()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(PageSizes.A4)
|
||||
.ProducePdf()
|
||||
.ShowResults()
|
||||
.Render(page =>
|
||||
{
|
||||
page.Padding(25).Layers(layers =>
|
||||
{
|
||||
layers.Layer().Image(Placeholders.Image);
|
||||
layers.PrimaryLayer().Padding(25).Image("multilingual.png");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DpiSetting()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(400, 600)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(page =>
|
||||
{
|
||||
page.Padding(10).Column(column =>
|
||||
{
|
||||
column.Spacing(10);
|
||||
|
||||
column.Item().Image("photo.jpg").WithRasterDpi(16);
|
||||
column.Item().Image("photo.jpg").WithRasterDpi(72);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompressionSetting()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(400, 600)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(page =>
|
||||
{
|
||||
page.Padding(10).Column(column =>
|
||||
{
|
||||
column.Spacing(10);
|
||||
|
||||
column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.VeryLow).WithRasterDpi(72);
|
||||
column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.High).WithRasterDpi(72);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Exception()
|
||||
{
|
||||
@@ -120,5 +120,42 @@ namespace QuestPDF.Examples
|
||||
.Background(Placeholders.BackgroundColor());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RepeatingInlinedInHeader_Test()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(document =>
|
||||
{
|
||||
document.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(1, Unit.Inch);
|
||||
page.PageColor(Colors.White);
|
||||
|
||||
page.Header()
|
||||
.Inlined(inlined =>
|
||||
{
|
||||
inlined.Spacing(10);
|
||||
|
||||
foreach (var i in Enumerable.Range(5, 5))
|
||||
inlined.Item().Width(i * 10).Height(20).Background(Colors.Red.Medium);
|
||||
});
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(20)
|
||||
.Column(column =>
|
||||
{
|
||||
column.Spacing(25);
|
||||
|
||||
foreach (var i in Enumerable.Range(10, 20))
|
||||
column.Item().Width(i * 10).Height(50).Background(Colors.Grey.Lighten2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
[SetUpFixture]
|
||||
public class LicenseSetup
|
||||
{
|
||||
[OneTimeSetUp]
|
||||
public static void Setup()
|
||||
{
|
||||
QuestPDF.Settings.License = LicenseType.Community;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,12 @@ namespace QuestPDF.Examples
|
||||
{
|
||||
var url = "https://picsum.photos/300/200";
|
||||
|
||||
if(Greyscale)
|
||||
if (Greyscale)
|
||||
url += "?grayscale";
|
||||
|
||||
using var client = new WebClient();
|
||||
client.Headers.Add("user-agent", "QuestPDF/1.0 Unit Testing");
|
||||
|
||||
var response = client.DownloadData(url);
|
||||
container.Image(response);
|
||||
}
|
||||
@@ -59,4 +61,4 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
|
||||
<PackageReference Include="microcharts" Version="0.9.5.9" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.3" />
|
||||
<PackageReference Include="Svg.Skia" Version="0.5.18" />
|
||||
@@ -32,6 +32,12 @@
|
||||
<None Update="pdf-icon.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="multilingual.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="photo.jpg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
@@ -299,21 +300,74 @@ namespace QuestPDF.Examples
|
||||
columns.RelativeColumn();
|
||||
columns.RelativeColumn();
|
||||
columns.RelativeColumn();
|
||||
columns.RelativeColumn();
|
||||
columns.RelativeColumn();
|
||||
});
|
||||
|
||||
table.Cell().RowSpan(4).Element(Block).Text("1");
|
||||
var image = Placeholders.Image(400, 300);
|
||||
|
||||
table.Cell().RowSpan(2).Element(Block).Text("2");
|
||||
table.Cell().RowSpan(1).Element(Block).Text("3");
|
||||
table.Cell().RowSpan(1).Element(Block).Text("4");
|
||||
|
||||
table.Cell().RowSpan(2).Element(Block).Text("5");
|
||||
table.Cell().RowSpan(1).Element(Block).Text("6");
|
||||
table.Cell().RowSpan(1).Element(Block).Text("7");
|
||||
table.Cell().Image(image);
|
||||
table.Cell().Image(image);
|
||||
table.Cell().Image(image);
|
||||
table.Cell().Image(image);
|
||||
table.Cell().Image(image);
|
||||
table.Cell().Image(image);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Bug_RowSpanWorksIncorrectly()
|
||||
{
|
||||
// https://github.com/QuestPDF/QuestPDF/issues/552
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.PageSize(PageSizes.A5.Landscape())
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container.Padding(20).Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
{
|
||||
columns.RelativeColumn(1);
|
||||
columns.RelativeColumn(2);
|
||||
columns.RelativeColumn(2);
|
||||
columns.RelativeColumn(2);
|
||||
});
|
||||
|
||||
foreach (var i in Enumerable.Range(6, 9))
|
||||
{
|
||||
table.Cell().Element(CellStyleMainTable).Text($"{i:00}:00");
|
||||
|
||||
foreach (var j in Enumerable.Range(1, 3))
|
||||
table.Cell().Element(CellStyleMainTable).Text("");
|
||||
}
|
||||
|
||||
static IContainer CellStyleMainTable(IContainer container)
|
||||
{
|
||||
return container
|
||||
.Border(0.5f).BorderColor(Colors.Blue.Lighten4)
|
||||
.Background(Colors.Blue.Lighten5)
|
||||
.PaddingVertical(5);
|
||||
}
|
||||
|
||||
table.Cell().Row(1).RowSpan(3).Column(2).Element(BlockAccepted).Text("3 rows");
|
||||
table.Cell().Row(1).RowSpan(6).Column(3).Element(BlockAccepted).Text("6 rows");
|
||||
table.Cell().Row(3).RowSpan(5).Column(4).Element(BlockAccepted).Text("5 rows");
|
||||
|
||||
static IContainer BlockAccepted(IContainer container)
|
||||
{
|
||||
return container
|
||||
.Border(1.5f).BorderColor(Colors.Green.Lighten2)
|
||||
.Background(Colors.Green.Lighten4);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TableHeader()
|
||||
{
|
||||
@@ -389,12 +443,12 @@ namespace QuestPDF.Examples
|
||||
table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().Text(page.name);
|
||||
|
||||
// inches
|
||||
table.Cell().Element(CellStyle).Text(page.width);
|
||||
table.Cell().Element(CellStyle).Text(page.height);
|
||||
table.Cell().Element(CellStyle).Text(page.width.ToString(CultureInfo.InvariantCulture));
|
||||
table.Cell().Element(CellStyle).Text(page.height.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
// points
|
||||
table.Cell().Element(CellStyle).Text(page.width * inchesToPoints);
|
||||
table.Cell().Element(CellStyle).Text(page.height * inchesToPoints);
|
||||
table.Cell().Element(CellStyle).Text((page.width * inchesToPoints).ToString(CultureInfo.InvariantCulture));
|
||||
table.Cell().Element(CellStyle).Text((page.height * inchesToPoints).ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.White);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
@@ -122,7 +123,132 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void LetterSpacing()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(500, 700)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(20)
|
||||
.Column(column =>
|
||||
{
|
||||
var letterSpacing = new[] { -0.1f, 0f, 0.2f };
|
||||
var paragraph = Placeholders.Sentence().ToUpper();
|
||||
|
||||
foreach (var spacing in letterSpacing)
|
||||
{
|
||||
column
|
||||
.Item()
|
||||
.Border(1)
|
||||
.Padding(10)
|
||||
.Column(nestedColumn =>
|
||||
{
|
||||
nestedColumn.Item()
|
||||
.Text(paragraph)
|
||||
.FontSize(16)
|
||||
.LetterSpacing(spacing);
|
||||
|
||||
nestedColumn.Item()
|
||||
.Text($"Letter spacing of {spacing} em")
|
||||
.FontSize(10)
|
||||
.Italic()
|
||||
.FontColor(Colors.Blue.Medium);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LetterSpacing_Arabic()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(500, 700)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(50)
|
||||
.Column(column =>
|
||||
{
|
||||
var letterSpacing = new[] { -0.1f, 0f, 0.2f };
|
||||
var paragraph = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
|
||||
foreach (var spacing in letterSpacing)
|
||||
{
|
||||
column
|
||||
.Item()
|
||||
.Border(1)
|
||||
.Padding(10)
|
||||
.Column(nestedColumn =>
|
||||
{
|
||||
nestedColumn.Item()
|
||||
.Text(paragraph)
|
||||
.FontSize(16)
|
||||
.FontFamily(Fonts.Calibri)
|
||||
.LetterSpacing(spacing);
|
||||
|
||||
nestedColumn.Item()
|
||||
.Text($"Letter spacing of {spacing} em")
|
||||
.FontSize(10)
|
||||
.Italic()
|
||||
.FontColor(Colors.Blue.Medium);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void LetterSpacing_Unicode()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(500, 700)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(50)
|
||||
.Column(column =>
|
||||
{
|
||||
var letterSpacing = new[] { 0f, 0.5f };
|
||||
|
||||
|
||||
|
||||
var paragraph = "Ţ̴̡̧̤̮̺̤̗͎̱̹͙͎͖͂̿̓́̉̊̀̍͜h̵̞̘͇̾̎̏̅į̵̹̖͔͉̰̎̉̄̐̏͑͂̅̃̃͘͝s̷͓͉̭̭̯̬̥̻̰̩̦̑̀̀͌́̒̍̒̌̇͛̀͛́̎ ̷̡̡̟͕̳̺̝̼͇͔̬̟̖͍̈́̽͜͝͝i̶͔͚̟̊̐͛́͛̄̌ṡ̸̡̤̪͙͍̥͙̟̼̝̰̥͈̿̓̄̿̓͠ ̶̢̦̙͍̯̖̱̰̯͕͔͎̯̝̎͑t̸͖̲̱̼̎͐̎̉̾̎̾̌̅̔̏͘ȩ̶̝̫̙͓̙̣̔̀̌̔̋̂̑̈́̏̀̈͘̕͜͝s̸̫̝̮̻̼͐̅̄̎̎̑͝ț̷̨̢̨̻͈̮̞̆͗̓͊̃̌͂̑̉̕̕͜͝͝";
|
||||
|
||||
|
||||
|
||||
foreach (var spacing in letterSpacing)
|
||||
{
|
||||
column.Item()
|
||||
.Text($"Letter spacing of {spacing} em")
|
||||
.FontSize(10)
|
||||
.Italic()
|
||||
.FontColor(Colors.Blue.Medium);
|
||||
|
||||
column.Item()
|
||||
.PaddingVertical(50)
|
||||
.Text(paragraph)
|
||||
.FontSize(16)
|
||||
.FontFamily(Fonts.Calibri)
|
||||
.LetterSpacing(spacing);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SuperscriptSubscript_Simple()
|
||||
{
|
||||
@@ -523,7 +649,7 @@ namespace QuestPDF.Examples
|
||||
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
column.Item().Text(null);
|
||||
column.Item().Text((string) null);
|
||||
|
||||
column.Item().Text(text =>
|
||||
{
|
||||
@@ -555,7 +681,7 @@ namespace QuestPDF.Examples
|
||||
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
column.Item().Text(null);
|
||||
column.Item().Text((string) null);
|
||||
|
||||
column.Item().Text(text =>
|
||||
{
|
||||
@@ -786,5 +912,132 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DetectSpanPositionExample()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(new PageSize(650, 800))
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
var fontSize = 20;
|
||||
|
||||
var paint = new SKPaint
|
||||
{
|
||||
Color = SKColors.Red,
|
||||
TextSize = fontSize
|
||||
};
|
||||
|
||||
var fontMetrics = paint.FontMetrics;
|
||||
|
||||
var start = 0f;
|
||||
var end = 0f;
|
||||
|
||||
// corner case: what if text is paged? clamp start and end?
|
||||
|
||||
container
|
||||
.Padding(25)
|
||||
.DefaultTextStyle(x => x.FontSize(fontSize).FontFamily("Calibri"))
|
||||
.Layers(layers =>
|
||||
{
|
||||
layers.PrimaryLayer().Text(text =>
|
||||
{
|
||||
text.Span(Placeholders.Paragraph());
|
||||
text.Span(" - ");
|
||||
|
||||
// record start
|
||||
text.Element().Width(0).Height(0)
|
||||
.Canvas((canvas, size) => start = canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
|
||||
|
||||
text.Span(Placeholders.LoremIpsum()).BackgroundColor(Colors.Red.Lighten4);
|
||||
|
||||
// record end
|
||||
text.Element().Width(0).Height(0)
|
||||
.Canvas((canvas, size) => end = canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
|
||||
|
||||
text.Span(" - ");
|
||||
text.Span(Placeholders.Paragraph());
|
||||
});
|
||||
|
||||
layers.Layer().Canvas((canvas, size) =>
|
||||
{
|
||||
canvas.Save();
|
||||
|
||||
canvas.Translate(-canvas.TotalMatrix.TransX / canvas.TotalMatrix.ScaleX, -canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
|
||||
canvas.DrawRect(10, start + fontMetrics.Ascent, 5, end - start + (fontMetrics.Bottom - fontMetrics.Ascent), paint);
|
||||
|
||||
canvas.Restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InconsistentLineHeightWhenUsingNewLineTest()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(PageSizes.A4)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(20)
|
||||
.Background(Colors.Grey.Lighten4)
|
||||
.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(x => x.FontSize(16));
|
||||
|
||||
text.Line(Placeholders.Paragraph());
|
||||
text.Line("");
|
||||
text.Line(Placeholders.Paragraph());
|
||||
|
||||
text.Line(Placeholders.Label()).FontSize(48);
|
||||
|
||||
text.Line(Placeholders.Paragraph());
|
||||
text.Line("");
|
||||
text.Line(Placeholders.Paragraph());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontFallback_Nested()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(50);
|
||||
page.PageColor(Colors.White);
|
||||
page.Size(PageSizes.A5.Landscape());
|
||||
|
||||
page.DefaultTextStyle(x => x
|
||||
.FontSize(24)
|
||||
.Bold()
|
||||
.FontFamily("Times New Roman")
|
||||
.Fallback(y => y
|
||||
.FontFamily("Microsoft YaHei")
|
||||
.Underline()
|
||||
.BackgroundColor(Colors.Red.Lighten2)));
|
||||
|
||||
page.Content().Text(text =>
|
||||
{
|
||||
text.Line("Default times new roman 中文文本 text.");
|
||||
text.Line("Normal weight and green 中文文本 text.").NormalWeight().BackgroundColor(Colors.Green.Lighten2);
|
||||
text.Line("Strikethrough without underline 中文文本 text.").Strikethrough().Underline(false);
|
||||
text.Line("Lato italic 中文文本 text.").FontFamily("Lato").Italic();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,62 @@
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer;
|
||||
using QuestPDF.ReportSample;
|
||||
using QuestPDF.ReportSample.Layouts;
|
||||
|
||||
//ImagePlaceholder.Solid = true;
|
||||
|
||||
// var model = DataSource.GetReport();
|
||||
// var report = new StandardReport(model);
|
||||
// report.ShowInPreviewer();
|
||||
//
|
||||
// return;
|
||||
|
||||
Document
|
||||
.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header()
|
||||
.Text("Hot Reload!")
|
||||
.SemiBold().FontSize(36).FontColor(Colors.Blue.Darken2);
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(20);
|
||||
|
||||
x.Item().Table(t =>
|
||||
{
|
||||
t.ColumnsDefinition(c =>
|
||||
{
|
||||
c.RelativeColumn();
|
||||
c.RelativeColumn(3);
|
||||
});
|
||||
|
||||
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Visual Studio");
|
||||
t.Cell().Border(1).Padding(5).Text("Start in debug mode with 'Hot Reload on Save' enabled.");
|
||||
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Command line");
|
||||
t.Cell().Border(1).Padding(5).Text("Run 'dotnet watch'.");
|
||||
});
|
||||
|
||||
x.Item().Text("Modify this line and the preview should show your changes instantly.");
|
||||
});
|
||||
|
||||
page.Footer()
|
||||
.AlignCenter()
|
||||
.Text(x =>
|
||||
{
|
||||
x.Span("Page ");
|
||||
x.CurrentPageNumber();
|
||||
});
|
||||
});
|
||||
})
|
||||
.ShowInPreviewer();
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
|
||||
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class CommunicationService
|
||||
{
|
||||
public static CommunicationService Instance { get; } = new ();
|
||||
|
||||
public event Action<ICollection<PreviewPage>>? OnDocumentRefreshed;
|
||||
|
||||
private WebApplication? Application { get; set; }
|
||||
|
||||
private readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private CommunicationService()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Task Start(int port)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging(x => x.ClearProviders());
|
||||
builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null);
|
||||
Application = builder.Build();
|
||||
|
||||
Application.MapGet("ping", HandlePing);
|
||||
Application.MapGet("version", HandleVersion);
|
||||
Application.MapPost("update/preview", HandleUpdatePreview);
|
||||
|
||||
return Application.RunAsync($"http://localhost:{port}/");
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
{
|
||||
await Application.StopAsync();
|
||||
await Application.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task<IResult> HandlePing()
|
||||
{
|
||||
return OnDocumentRefreshed == null
|
||||
? Results.StatusCode(StatusCodes.Status503ServiceUnavailable)
|
||||
: Results.Ok();
|
||||
}
|
||||
|
||||
private async Task<IResult> HandleVersion()
|
||||
{
|
||||
return Results.Json(GetType().Assembly.GetName().Version);
|
||||
}
|
||||
|
||||
private async Task<IResult> HandleUpdatePreview(HttpRequest request)
|
||||
{
|
||||
var command = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
|
||||
|
||||
var pages = command
|
||||
.Pages
|
||||
.Select(page =>
|
||||
{
|
||||
using var stream = request.Form.Files[page.Id].OpenReadStream();
|
||||
var picture = SKPicture.Deserialize(stream);
|
||||
|
||||
return new PreviewPage(picture, page.Width, page.Height);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
Task.Run(() => OnDocumentRefreshed(pages));
|
||||
return Results.Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class Helpers
|
||||
{
|
||||
public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> pages)
|
||||
{
|
||||
using var stream = File.Create(filePath);
|
||||
|
||||
using var document = SKDocument.CreatePdf(stream);
|
||||
|
||||
foreach (var page in pages)
|
||||
{
|
||||
using var canvas = document.BeginPage(page.Width, page.Height);
|
||||
canvas.DrawPicture(page.Picture);
|
||||
document.EndPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Rendering.SceneGraph;
|
||||
using Avalonia.Skia;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class InteractiveCanvas : ICustomDrawOperation
|
||||
{
|
||||
public Rect Bounds { get; set; }
|
||||
public ICollection<PreviewPage> Pages { get; set; }
|
||||
|
||||
private float Width => (float)Bounds.Width;
|
||||
private float Height => (float)Bounds.Height;
|
||||
|
||||
public float Scale { get; private set; } = 1;
|
||||
public float TranslateX { get; set; }
|
||||
public float TranslateY { get; set; }
|
||||
|
||||
private const float MinScale = 0.1f;
|
||||
private const float MaxScale = 10f;
|
||||
|
||||
private const float PageSpacing = 25f;
|
||||
private const float SafeZone = 25f;
|
||||
|
||||
public float TotalPagesHeight => Pages.Sum(x => x.Height) + (Pages.Count - 1) * PageSpacing;
|
||||
public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
|
||||
public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
|
||||
|
||||
public float MaxTranslateY => TotalHeight - Height / Scale;
|
||||
|
||||
public float ScrollPercentY
|
||||
{
|
||||
get
|
||||
{
|
||||
return TranslateY / MaxTranslateY;
|
||||
}
|
||||
set
|
||||
{
|
||||
TranslateY = value * MaxTranslateY;
|
||||
}
|
||||
}
|
||||
|
||||
public float ScrollViewportSizeY
|
||||
{
|
||||
get
|
||||
{
|
||||
var viewPortSize = Height / Scale / TotalHeight;
|
||||
return Math.Clamp(viewPortSize, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#region transformations
|
||||
|
||||
private void LimitScale()
|
||||
{
|
||||
Scale = Math.Max(Scale, MinScale);
|
||||
Scale = Math.Min(Scale, MaxScale);
|
||||
}
|
||||
|
||||
private void LimitTranslate()
|
||||
{
|
||||
if (TotalPagesHeight > Height / Scale)
|
||||
{
|
||||
TranslateY = Math.Min(TranslateY, MaxTranslateY);
|
||||
TranslateY = Math.Max(TranslateY, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateY = (TotalPagesHeight - Height / Scale) / 2;
|
||||
}
|
||||
|
||||
if (Width / Scale < MaxWidth)
|
||||
{
|
||||
var maxTranslateX = (Width / 2 - SafeZone) / Scale - MaxWidth / 2;
|
||||
|
||||
TranslateX = Math.Min(TranslateX, -maxTranslateX);
|
||||
TranslateX = Math.Max(TranslateX, maxTranslateX);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateX = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void TranslateWithCurrentScale(float x, float y)
|
||||
{
|
||||
TranslateX += x / Scale;
|
||||
TranslateY += y / Scale;
|
||||
|
||||
LimitTranslate();
|
||||
}
|
||||
|
||||
public void ZoomToPoint(float x, float y, float factor)
|
||||
{
|
||||
var oldScale = Scale;
|
||||
Scale *= factor;
|
||||
|
||||
LimitScale();
|
||||
|
||||
TranslateX -= x / Scale - x / oldScale;
|
||||
TranslateY -= y / Scale - y / oldScale;
|
||||
|
||||
LimitTranslate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region rendering
|
||||
|
||||
public void Render(IDrawingContextImpl context)
|
||||
{
|
||||
if (Pages.Count <= 0)
|
||||
return;
|
||||
|
||||
LimitScale();
|
||||
LimitTranslate();
|
||||
|
||||
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
|
||||
|
||||
if (canvas == null)
|
||||
throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
|
||||
|
||||
var originalMatrix = canvas.TotalMatrix;
|
||||
|
||||
canvas.Translate(Width / 2, 0);
|
||||
|
||||
canvas.Scale(Scale);
|
||||
canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
|
||||
|
||||
foreach (var page in Pages)
|
||||
{
|
||||
canvas.Translate(-page.Width / 2f, 0);
|
||||
DrawBlankPage(canvas, page.Width, page.Height);
|
||||
canvas.DrawPicture(page.Picture);
|
||||
canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
|
||||
}
|
||||
|
||||
canvas.SetMatrix(originalMatrix);
|
||||
DrawInnerGradient(canvas);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public bool Equals(ICustomDrawOperation? other) => false;
|
||||
public bool HitTest(Point p) => true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region blank page
|
||||
|
||||
private static SKPaint BlankPagePaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.White
|
||||
};
|
||||
|
||||
private static SKPaint BlankPageShadowPaint = new SKPaint
|
||||
{
|
||||
ImageFilter = SKImageFilter.CreateBlendMode(
|
||||
SKBlendMode.Overlay,
|
||||
SKImageFilter.CreateDropShadowOnly(0, 6, 6, 6, SKColors.Black.WithAlpha(64)),
|
||||
SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32)))
|
||||
};
|
||||
|
||||
private void DrawBlankPage(SKCanvas canvas, float width, float height)
|
||||
{
|
||||
canvas.DrawRect(0, 0, width, height, BlankPageShadowPaint);
|
||||
canvas.DrawRect(0, 0, width, height, BlankPagePaint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region inner viewport gradient
|
||||
|
||||
private const int InnerGradientSize = (int)SafeZone;
|
||||
private static readonly SKColor InnerGradientColor = SKColor.Parse("#666");
|
||||
|
||||
private void DrawInnerGradient(SKCanvas canvas)
|
||||
{
|
||||
// gamma correction
|
||||
var colors = Enumerable
|
||||
.Range(0, InnerGradientSize)
|
||||
.Select(x => 1f - x / (float) InnerGradientSize)
|
||||
.Select(x => Math.Pow(x, 2f))
|
||||
.Select(x => (byte)(x * 255))
|
||||
.Select(x => InnerGradientColor.WithAlpha(x))
|
||||
.ToArray();
|
||||
|
||||
using var fogPaint = new SKPaint
|
||||
{
|
||||
Shader = SKShader.CreateLinearGradient(
|
||||
new SKPoint(0, 0),
|
||||
new SKPoint(0, InnerGradientSize),
|
||||
colors,
|
||||
SKShaderTileMode.Clamp)
|
||||
};
|
||||
|
||||
canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
record PreviewPage(SKPicture Picture, float Width, float Height);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Name="QuestPDF Document Preview">
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Dark" />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -0,0 +1,27 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
internal class PreviewerApp : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new PreviewerWindow()
|
||||
{
|
||||
DataContext = new PreviewerWindowViewModel()
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||