Compare commits
28 Commits
2023.4
...
feature/wip
| Author | SHA1 | Date | |
|---|---|---|---|
| 82bc1e72c6 | |||
| f20d7af0c5 | |||
| 3a73886d70 | |||
| 045b2a43b8 | |||
| 312e94a4bd | |||
| fcef2b45d1 | |||
| 3606bc9e79 | |||
| 6fe6e7ee41 | |||
| c41d045eee | |||
| 9aa22fbeea | |||
| f1ff03fede | |||
| fc8a788cf7 | |||
| 5d2d97097c | |||
| 03e10d3d0a | |||
| 9bb1d27585 | |||
| 2b701e11d4 | |||
| 6b1de07caa | |||
| 461dcde141 | |||
| 8622633e34 | |||
| 90565650c9 | |||
| d1a7018748 | |||
| 1829ed7180 | |||
| bdb61aa545 | |||
| c21a335342 | |||
| ad47b82b8f | |||
| 2040822a37 | |||
| 6d1cd81a1d | |||
| 26195786a4 |
@@ -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
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -316,6 +316,58 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -4,7 +4,7 @@
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF.Previewer</PackageId>
|
||||
<Version>2023.4.0</Version>
|
||||
<Version>2023.5.0</Version>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<ToolCommandName>questpdf-previewer</ToolCommandName>
|
||||
<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>
|
||||
@@ -37,12 +37,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.19" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.19" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.19" />
|
||||
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
|
||||
<PackageReference Include="ReactiveUI" Version="18.4.1" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.19" />
|
||||
<PackageReference Include="ReactiveUI" Version="18.4.44" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.3" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Win32" Version="2.88.3" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.3" />
|
||||
|
||||
@@ -21,24 +21,20 @@ namespace QuestPDF.ReportSample
|
||||
|
||||
var model = DataSource.GetReport();
|
||||
Report = new StandardReport(model);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateAndShowPdf()
|
||||
{
|
||||
|
||||
//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()
|
||||
public void GeneratePdfAndShow()
|
||||
{
|
||||
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.xps");
|
||||
Report.GenerateXps(path);
|
||||
Process.Start("explorer.exe", path);
|
||||
Report.GeneratePdfAndShow();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateXpsAndShow()
|
||||
{
|
||||
Report.GenerateXpsAndShow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ namespace QuestPDF.UnitTests
|
||||
TestPlan
|
||||
.For(x => new DynamicImage
|
||||
{
|
||||
TargetDpi = DocumentSettings.DefaultRasterDpi,
|
||||
CompressionQuality = ImageCompressionQuality.High,
|
||||
Source = GenerateImage
|
||||
})
|
||||
.MeasureElement(new Size(300, 200))
|
||||
@@ -29,6 +31,8 @@ namespace QuestPDF.UnitTests
|
||||
TestPlan
|
||||
.For(x => new DynamicImage
|
||||
{
|
||||
TargetDpi = DocumentSettings.DefaultRasterDpi,
|
||||
CompressionQuality = ImageCompressionQuality.High,
|
||||
Source = size => null
|
||||
})
|
||||
.DrawElement(new Size(300, 200))
|
||||
@@ -41,6 +45,8 @@ namespace QuestPDF.UnitTests
|
||||
TestPlan
|
||||
.For(x => new DynamicImage
|
||||
{
|
||||
TargetDpi = DocumentSettings.DefaultRasterDpi,
|
||||
CompressionQuality = ImageCompressionQuality.High,
|
||||
Source = GenerateImage
|
||||
})
|
||||
.DrawElement(new Size(300, 200))
|
||||
@@ -51,11 +57,13 @@ namespace QuestPDF.UnitTests
|
||||
[Test]
|
||||
public void Draw_PassesCorrectSizeToSource()
|
||||
{
|
||||
Size passedSize = default;
|
||||
ImageSize passedSize = default;
|
||||
|
||||
TestPlan
|
||||
.For(x => new DynamicImage
|
||||
{
|
||||
TargetDpi = DocumentSettings.DefaultRasterDpi * 3,
|
||||
CompressionQuality = ImageCompressionQuality.High,
|
||||
Source = size =>
|
||||
{
|
||||
passedSize = size;
|
||||
@@ -66,12 +74,13 @@ namespace QuestPDF.UnitTests
|
||||
.ExpectCanvasDrawImage(Position.Zero, new Size(400, 300))
|
||||
.CheckDrawResult();
|
||||
|
||||
passedSize.Should().BeEquivalentTo(new Size(400, 300));
|
||||
passedSize.Width.Should().Be(1200);
|
||||
passedSize.Height.Should().Be(900);
|
||||
}
|
||||
|
||||
byte[] GenerateImage(Size size)
|
||||
byte[] GenerateImage(ImageSize size)
|
||||
{
|
||||
var image = GenerateImage((int) size.Width, (int) size.Height);
|
||||
var image = GenerateImage(size.Width, size.Height);
|
||||
return image.Encode(SKEncodedImageFormat.Png, 100).ToArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.UnitTests.TestEngine;
|
||||
using SkiaSharp;
|
||||
using ImageElement = QuestPDF.Elements.Image;
|
||||
using DocumentImage = QuestPDF.Infrastructure.Image;
|
||||
|
||||
namespace QuestPDF.UnitTests
|
||||
{
|
||||
@@ -15,9 +23,9 @@ namespace QuestPDF.UnitTests
|
||||
public void Measure_TakesAvailableSpaceRegardlessOfSize()
|
||||
{
|
||||
TestPlan
|
||||
.For(x => new Image
|
||||
.For(x => new ImageElement
|
||||
{
|
||||
InternalImage = GenerateImage(400, 300)
|
||||
DocumentImage = GenerateDocumentImage(400, 300)
|
||||
})
|
||||
.MeasureElement(new Size(300, 200))
|
||||
.CheckMeasureResult(SpacePlan.FullRender(300, 200));
|
||||
@@ -27,9 +35,11 @@ namespace QuestPDF.UnitTests
|
||||
public void Draw_TakesAvailableSpaceRegardlessOfSize()
|
||||
{
|
||||
TestPlan
|
||||
.For(x => new Image
|
||||
.For(x => new ImageElement
|
||||
{
|
||||
InternalImage = GenerateImage(400, 300)
|
||||
CompressionQuality = ImageCompressionQuality.High,
|
||||
TargetDpi = DocumentSettings.DefaultRasterDpi,
|
||||
DocumentImage = GenerateDocumentImage(400, 300)
|
||||
})
|
||||
.DrawElement(new Size(300, 200))
|
||||
.ExpectCanvasDrawImage(new Position(0, 0), new Size(300, 200))
|
||||
@@ -39,7 +49,7 @@ namespace QuestPDF.UnitTests
|
||||
[Test]
|
||||
public void Fluent_RecognizesImageProportions()
|
||||
{
|
||||
var image = GenerateImage(600, 200).Encode(SKEncodedImageFormat.Png, 100).ToArray();
|
||||
var image = GenerateDocumentImage(600, 200);
|
||||
|
||||
TestPlan
|
||||
.For(x =>
|
||||
@@ -52,11 +62,80 @@ namespace QuestPDF.UnitTests
|
||||
.CheckMeasureResult(SpacePlan.FullRender(300, 100));;
|
||||
}
|
||||
|
||||
SKImage GenerateImage(int width, int height)
|
||||
[Test]
|
||||
public void UsingSharedImageShouldNotDrasticallyIncreaseDocumentSize()
|
||||
{
|
||||
var imageInfo = new SKImageInfo(width, height);
|
||||
using var surface = SKSurface.Create(imageInfo);
|
||||
return surface.Snapshot();
|
||||
var photo = File.ReadAllBytes("Resources/photo.jpg");
|
||||
|
||||
var documentWithSingleImageSize = GetDocumentSize(container =>
|
||||
{
|
||||
container.Image(photo);
|
||||
});
|
||||
|
||||
var documentWithMultipleImagesSize = GetDocumentSize(container =>
|
||||
{
|
||||
container.Column(column =>
|
||||
{
|
||||
foreach (var i in Enumerable.Range(0, 10))
|
||||
column.Item().Image(photo);
|
||||
});
|
||||
});
|
||||
|
||||
var documentWithSingleImageUsedMultipleTimesSize = GetDocumentSize(container =>
|
||||
{
|
||||
container.Column(column =>
|
||||
{
|
||||
var sharedImage = DocumentImage.FromBinaryData(photo).DisposeAfterDocumentGeneration();
|
||||
|
||||
foreach (var i in Enumerable.Range(0, 10))
|
||||
column.Item().Image(sharedImage);
|
||||
});
|
||||
});
|
||||
|
||||
(documentWithMultipleImagesSize / (float)documentWithSingleImageSize).Should().BeInRange(9.9f, 10);
|
||||
(documentWithSingleImageUsedMultipleTimesSize / (float)documentWithSingleImageSize).Should().BeInRange(1f, 1.05f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ImageCompressionHasImpactOnDocumentSize()
|
||||
{
|
||||
var photo = File.ReadAllBytes("Resources/photo.jpg");
|
||||
|
||||
var veryLowCompressionSize = GetDocumentSize(container => container.Image(photo).WithCompressionQuality(ImageCompressionQuality.VeryLow));
|
||||
var bestCompressionSize = GetDocumentSize(container => container.Image(photo).WithCompressionQuality(ImageCompressionQuality.Best));
|
||||
|
||||
(bestCompressionSize / (float)veryLowCompressionSize).Should().BeGreaterThan(25);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TargetDpiHasImpactOnDocumentSize()
|
||||
{
|
||||
var photo = File.ReadAllBytes("Resources/photo.jpg");
|
||||
|
||||
var lowDpiSize = GetDocumentSize(container => container.Image(photo).WithRasterDpi(12));
|
||||
var highDpiSize = GetDocumentSize(container => container.Image(photo).WithRasterDpi(144));
|
||||
|
||||
(highDpiSize / (float)lowDpiSize).Should().BeGreaterThan(40);
|
||||
}
|
||||
|
||||
private static int GetDocumentSize(Action<IContainer> container)
|
||||
{
|
||||
return Document
|
||||
.Create(document =>
|
||||
{
|
||||
document.Page(page =>
|
||||
{
|
||||
page.Content().Element(container);
|
||||
});
|
||||
})
|
||||
.GeneratePdf()
|
||||
.Length;
|
||||
}
|
||||
|
||||
DocumentImage GenerateDocumentImage(int width, int height)
|
||||
{
|
||||
var image = Placeholders.Image(width, height);
|
||||
return DocumentImage.FromBinaryData(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.UnitTests
|
||||
{
|
||||
[SetUpFixture]
|
||||
public class LicenseSetup
|
||||
{
|
||||
[OneTimeSetUp]
|
||||
public static void Setup()
|
||||
{
|
||||
QuestPDF.Settings.License = LicenseType.Community;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,4 +27,10 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Resources\photo.jpg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -237,6 +237,8 @@ namespace QuestPDF.UnitTests.TestEngine
|
||||
|
||||
Child = new DynamicImage
|
||||
{
|
||||
CompressionQuality = ImageCompressionQuality.Medium,
|
||||
TargetDpi = DocumentSettings.DefaultRasterDpi,
|
||||
Source = Placeholders.Image
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,8 +21,9 @@ namespace QuestPDF.Drawing
|
||||
CheckIfStreamIsCompatible(stream);
|
||||
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new PdfCanvas(stream, metadata);
|
||||
RenderDocument(canvas, document);
|
||||
var settings = document.GetSettings();
|
||||
var canvas = new PdfCanvas(stream, metadata, settings);
|
||||
RenderDocument(canvas, document, settings);
|
||||
}
|
||||
|
||||
internal static void GenerateXps(Stream stream, IDocument document)
|
||||
@@ -30,9 +31,9 @@ namespace QuestPDF.Drawing
|
||||
ValidateLicense();
|
||||
CheckIfStreamIsCompatible(stream);
|
||||
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new XpsCanvas(stream, metadata);
|
||||
RenderDocument(canvas, document);
|
||||
var settings = document.GetSettings();
|
||||
var canvas = new XpsCanvas(stream, settings);
|
||||
RenderDocument(canvas, document, settings);
|
||||
}
|
||||
|
||||
private static void CheckIfStreamIsCompatible(Stream stream)
|
||||
@@ -44,13 +45,14 @@ namespace QuestPDF.Drawing
|
||||
throw new ArgumentException("The library requires a Stream object with the 'seek' capability available (the CanSeek flag). Please consider using the MemoryStream class.");
|
||||
}
|
||||
|
||||
internal static ICollection<byte[]> GenerateImages(IDocument document)
|
||||
internal static ICollection<byte[]> GenerateImages(IDocument document, ImageGenerationSettings settings)
|
||||
{
|
||||
ValidateLicense();
|
||||
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new ImageCanvas(metadata);
|
||||
RenderDocument(canvas, document);
|
||||
var documentSettings = document.GetSettings();
|
||||
documentSettings.ImageRasterDpi = settings.RasterDpi;
|
||||
var canvas = new ImageCanvas(settings);
|
||||
RenderDocument(canvas, document, documentSettings);
|
||||
|
||||
return canvas.Images;
|
||||
}
|
||||
@@ -87,18 +89,20 @@ namespace QuestPDF.Drawing
|
||||
internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
|
||||
{
|
||||
var canvas = new SkiaPictureCanvas();
|
||||
RenderDocument(canvas, document);
|
||||
RenderDocument(canvas, document, DocumentSettings.Default);
|
||||
return canvas.Pictures;
|
||||
}
|
||||
|
||||
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
|
||||
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings)
|
||||
where TCanvas : ICanvas, IRenderingCanvas
|
||||
{
|
||||
var container = new DocumentContainer();
|
||||
document.Compose(container);
|
||||
var content = container.Compose();
|
||||
|
||||
ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default);
|
||||
ApplyContentDirection(content, ContentDirection.LeftToRight);
|
||||
ApplyContentDirection(content, settings.ContentDirection);
|
||||
ApplyDefaultImageConfiguration(content, settings.ImageRasterDpi, settings.ImageCompressionQuality);
|
||||
|
||||
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
|
||||
|
||||
@@ -224,6 +228,38 @@ namespace QuestPDF.Drawing
|
||||
foreach (var child in content.GetChildren())
|
||||
ApplyContentDirection(child, direction);
|
||||
}
|
||||
|
||||
internal static void ApplyDefaultImageConfiguration(this Element? content, int imageRasterDpi, ImageCompressionQuality imageCompressionQuality)
|
||||
{
|
||||
content.VisitChildren(x =>
|
||||
{
|
||||
if (x is QuestPDF.Elements.Image image)
|
||||
{
|
||||
image.TargetDpi ??= imageRasterDpi;
|
||||
image.CompressionQuality ??= imageCompressionQuality;
|
||||
}
|
||||
|
||||
if (x is QuestPDF.Elements.DynamicImage dynamicImage)
|
||||
{
|
||||
dynamicImage.TargetDpi ??= imageRasterDpi;
|
||||
dynamicImage.CompressionQuality ??= imageCompressionQuality;
|
||||
}
|
||||
|
||||
if (x is DynamicHost dynamicHost)
|
||||
{
|
||||
dynamicHost.ImageTargetDpi ??= imageRasterDpi;
|
||||
dynamicHost.ImageCompressionQuality ??= imageCompressionQuality;
|
||||
}
|
||||
|
||||
if (x is TextBlock textBlock)
|
||||
{
|
||||
foreach (var textBlockElement in textBlock.Items.OfType<TextBlockElement>())
|
||||
{
|
||||
textBlockElement.Element.ApplyDefaultImageConfiguration(imageRasterDpi, imageCompressionQuality);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
internal static void ApplyInheritedAndGlobalTexStyle(this Element? content, TextStyle documentDefaultTextStyle)
|
||||
{
|
||||
|
||||
@@ -7,14 +7,14 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
internal class ImageCanvas : SkiaCanvasBase
|
||||
{
|
||||
private DocumentMetadata Metadata { get; }
|
||||
private ImageGenerationSettings Settings { get; }
|
||||
private SKSurface Surface { get; set; }
|
||||
|
||||
internal ICollection<byte[]> Images { get; } = new List<byte[]>();
|
||||
|
||||
public ImageCanvas(DocumentMetadata metadata)
|
||||
public ImageCanvas(ImageGenerationSettings settings)
|
||||
{
|
||||
Metadata = metadata;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public override void BeginDocument()
|
||||
@@ -30,7 +30,7 @@ namespace QuestPDF.Drawing
|
||||
|
||||
public override void BeginPage(Size size)
|
||||
{
|
||||
var scalingFactor = Metadata.RasterDpi / (float) PageSizes.PointsPerInch;
|
||||
var scalingFactor = Settings.RasterDpi / (float) PageSizes.PointsPerInch;
|
||||
var imageInfo = new SKImageInfo((int) (size.Width * scalingFactor), (int) (size.Height * scalingFactor));
|
||||
|
||||
Surface = SKSurface.Create(imageInfo);
|
||||
@@ -42,7 +42,7 @@ namespace QuestPDF.Drawing
|
||||
public override void EndPage()
|
||||
{
|
||||
Canvas.Save();
|
||||
var image = Surface.Snapshot().Encode(SKEncodedImageFormat.Png, 100).ToArray();
|
||||
var image = Surface.Snapshot().Encode(Settings.Format, Settings.Quality).ToArray();
|
||||
Images.Add(image);
|
||||
|
||||
Canvas.Dispose();
|
||||
|
||||
@@ -2,23 +2,24 @@
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
internal class PdfCanvas : SkiaDocumentCanvasBase
|
||||
{
|
||||
public PdfCanvas(Stream stream, DocumentMetadata documentMetadata)
|
||||
: base(CreatePdf(stream, documentMetadata))
|
||||
public PdfCanvas(Stream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
|
||||
: base(CreatePdf(stream, documentMetadata, documentSettings))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata)
|
||||
private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata));
|
||||
return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata, documentSettings));
|
||||
}
|
||||
catch (TypeInitializationException exception)
|
||||
{
|
||||
@@ -26,7 +27,7 @@ namespace QuestPDF.Drawing
|
||||
}
|
||||
}
|
||||
|
||||
private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata)
|
||||
private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata, DocumentSettings documentSettings)
|
||||
{
|
||||
return new SKDocumentPdfMetadata
|
||||
{
|
||||
@@ -40,9 +41,9 @@ namespace QuestPDF.Drawing
|
||||
Creation = metadata.CreationDate,
|
||||
Modified = metadata.ModifiedDate,
|
||||
|
||||
RasterDpi = metadata.RasterDpi,
|
||||
EncodingQuality = metadata.ImageQuality,
|
||||
PdfA = metadata.PdfA
|
||||
RasterDpi = documentSettings.ImageRasterDpi,
|
||||
EncodingQuality = documentSettings.ImageCompressionQuality.ToQualityValue(),
|
||||
PdfA = documentSettings.PdfA
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,24 @@
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
internal class XpsCanvas : SkiaDocumentCanvasBase
|
||||
{
|
||||
public XpsCanvas(Stream stream, DocumentMetadata documentMetadata)
|
||||
: base(CreateXps(stream, documentMetadata))
|
||||
public XpsCanvas(Stream stream, DocumentSettings documentSettings)
|
||||
: base(CreateXps(stream, documentSettings))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private static SKDocument CreateXps(Stream stream, DocumentMetadata documentMetadata)
|
||||
private static SKDocument CreateXps(Stream stream, DocumentSettings documentSettings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SKDocument.CreateXps(stream, documentMetadata.RasterDpi);
|
||||
return SKDocument.CreateXps(stream, documentSettings.ImageRasterDpi);
|
||||
}
|
||||
catch (TypeInitializationException exception)
|
||||
{
|
||||
|
||||
@@ -8,11 +8,14 @@ namespace QuestPDF.Elements
|
||||
{
|
||||
public ContentDirection ContentDirection { get; set; }
|
||||
|
||||
public float Ratio { get; set; } = 1;
|
||||
public float Ratio { get; set; }
|
||||
public AspectRatioOption Option { get; set; } = AspectRatioOption.FitWidth;
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (Ratio == 0)
|
||||
return SpacePlan.FullRender(0, 0);
|
||||
|
||||
if(Child == null)
|
||||
return SpacePlan.FullRender(0, 0);
|
||||
|
||||
@@ -56,6 +59,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
private Size GetTargetSize(Size availableSpace)
|
||||
{
|
||||
if (Ratio == 0)
|
||||
return availableSpace;
|
||||
|
||||
var spaceRatio = availableSpace.Width / availableSpace.Height;
|
||||
|
||||
var fitHeight = new Size(availableSpace.Height * Ratio, availableSpace.Height) ;
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace QuestPDF.Elements
|
||||
internal TextStyle TextStyle { get; set; } = TextStyle.Default;
|
||||
public ContentDirection ContentDirection { get; set; }
|
||||
|
||||
internal int? ImageTargetDpi { get; set; }
|
||||
internal ImageCompressionQuality? ImageCompressionQuality { get; set; }
|
||||
|
||||
public DynamicHost(DynamicComponentProxy child)
|
||||
{
|
||||
Child = child;
|
||||
@@ -58,6 +61,9 @@ namespace QuestPDF.Elements
|
||||
TextStyle = TextStyle,
|
||||
ContentDirection = ContentDirection,
|
||||
|
||||
ImageTargetDpi = ImageTargetDpi.Value,
|
||||
ImageCompressionQuality = ImageCompressionQuality.Value,
|
||||
|
||||
PageNumber = PageContext.CurrentPage,
|
||||
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
|
||||
AvailableSize = availableSize
|
||||
@@ -79,7 +85,10 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal TextStyle TextStyle { get; set; }
|
||||
internal ContentDirection ContentDirection { get; set; }
|
||||
|
||||
|
||||
internal int ImageTargetDpi { get; set; }
|
||||
internal ImageCompressionQuality ImageCompressionQuality { get; set; }
|
||||
|
||||
public int PageNumber { get; internal set; }
|
||||
public int TotalPages { get; internal set; }
|
||||
public Size AvailableSize { get; internal set; }
|
||||
@@ -91,6 +100,7 @@ namespace QuestPDF.Elements
|
||||
|
||||
container.ApplyInheritedAndGlobalTexStyle(TextStyle);
|
||||
container.ApplyContentDirection(ContentDirection);
|
||||
container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality);
|
||||
|
||||
container.InjectDependencies(PageContext, Canvas);
|
||||
container.VisitChildren(x => (x as IStateResettable)?.ResetState());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
@@ -6,9 +7,13 @@ using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
public delegate byte[] GenerateDynamicImageDelegate(ImageSize size);
|
||||
|
||||
internal class DynamicImage : Element
|
||||
{
|
||||
public Func<Size, byte[]>? Source { get; set; }
|
||||
internal int? TargetDpi { get; set; }
|
||||
internal ImageCompressionQuality? CompressionQuality { get; set; }
|
||||
public GenerateDynamicImageDelegate? Source { get; set; }
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
@@ -19,13 +24,27 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
var imageData = Source?.Invoke(availableSpace);
|
||||
var targetResolution = GetTargetResolution(availableSpace, TargetDpi.Value);
|
||||
var imageData = Source?.Invoke(targetResolution);
|
||||
|
||||
if (imageData == null)
|
||||
return;
|
||||
|
||||
using var image = SKImage.FromEncodedData(imageData);
|
||||
Canvas.DrawImage(image, Position.Zero, availableSpace);
|
||||
using var originalImage = SKImage.FromEncodedData(imageData);
|
||||
using var compressedImage = originalImage.CompressImage(CompressionQuality.Value);
|
||||
|
||||
var targetImage = Helpers.Helpers.GetImageWithSmallerSize(originalImage, compressedImage);
|
||||
Canvas.DrawImage(targetImage, Position.Zero, availableSpace);
|
||||
}
|
||||
|
||||
private static ImageSize GetTargetResolution(Size availableSize, int targetDpi)
|
||||
{
|
||||
var scalingFactor = targetDpi / (float)DocumentSettings.DefaultRasterDpi;
|
||||
|
||||
return new ImageSize(
|
||||
(int)(availableSize.Width * scalingFactor),
|
||||
(int)(availableSize.Height * scalingFactor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@ namespace QuestPDF.Elements
|
||||
{
|
||||
internal class Image : Element, ICacheable
|
||||
{
|
||||
public SKImage? InternalImage { get; set; }
|
||||
public Infrastructure.Image? DocumentImage { get; set; }
|
||||
|
||||
internal bool UseOriginalImage { get; set; }
|
||||
internal int? TargetDpi { get; set; }
|
||||
internal ImageCompressionQuality? CompressionQuality { get; set; }
|
||||
|
||||
~Image()
|
||||
{
|
||||
InternalImage?.Dispose();
|
||||
if (DocumentImage is { IsDocumentScoped: true })
|
||||
DocumentImage?.Dispose();
|
||||
}
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
@@ -23,10 +28,44 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
if (InternalImage == null)
|
||||
if (DocumentImage == null)
|
||||
return;
|
||||
|
||||
Canvas.DrawImage(InternalImage, Position.Zero, availableSpace);
|
||||
var image = GetImageToDraw(availableSpace);
|
||||
Canvas.DrawImage(image, Position.Zero, availableSpace);
|
||||
}
|
||||
|
||||
private SKImage GetImageToDraw(Size availableSpace)
|
||||
{
|
||||
var originalImage = DocumentImage.SkImage;
|
||||
|
||||
if (UseOriginalImage)
|
||||
return originalImage;
|
||||
|
||||
var request = new GetImageVersionRequest
|
||||
{
|
||||
Resolution = GetTargetResolution(DocumentImage.Size, availableSpace, TargetDpi.Value),
|
||||
CompressionQuality = CompressionQuality.Value
|
||||
};
|
||||
|
||||
var targetImage = DocumentImage.GetVersionOfSize(request);
|
||||
return Helpers.Helpers.GetImageWithSmallerSize(originalImage, targetImage);
|
||||
}
|
||||
|
||||
private static ImageSize GetTargetResolution(ImageSize imageResolution, Size availableAreaSize, int targetDpi)
|
||||
{
|
||||
var scalingFactor = targetDpi / (float)DocumentSettings.DefaultRasterDpi;
|
||||
|
||||
var targetResolution = new ImageSize(
|
||||
(int)(availableAreaSize.Width * scalingFactor),
|
||||
(int)(availableAreaSize.Height * scalingFactor));
|
||||
|
||||
var isImageResolutionSmallerThanTarget = imageResolution.Width < targetResolution.Width || imageResolution.Height < targetResolution.Height;
|
||||
|
||||
if (isImageResolutionSmallerThanTarget)
|
||||
return imageResolution;
|
||||
|
||||
return targetResolution;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ namespace QuestPDF.Elements.Table
|
||||
UpdateColumnsWidth(availableSpace.Width);
|
||||
var renderingCommands = PlanLayout(availableSpace);
|
||||
|
||||
foreach (var command in renderingCommands)
|
||||
foreach (var command in renderingCommands.OrderBy(x => x.Cell.ZIndex))
|
||||
{
|
||||
if (command.Measurement.Type == SpacePlanType.FullRender)
|
||||
command.Cell.IsRendered = true;
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace QuestPDF.Elements.Table
|
||||
public int Column { get; set; } = 0;
|
||||
public int ColumnSpan { get; set; } = 1;
|
||||
|
||||
public int ZIndex { get; set; }
|
||||
|
||||
public bool IsRendered { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,13 @@ namespace QuestPDF.Elements.Table
|
||||
{
|
||||
var cellsWindow = new List<TableCell>();
|
||||
(int x, int y) currentLocation = (1, 1);
|
||||
var zIndex = 0;
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
cell.ZIndex = zIndex;
|
||||
zIndex++;
|
||||
|
||||
if (cellsWindow.Count > Math.Max(columnsCount, 16))
|
||||
{
|
||||
cellsWindow = cellsWindow
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
@@ -28,6 +29,17 @@ namespace QuestPDF.Fluent
|
||||
DocumentGenerator.GeneratePdf(stream, document);
|
||||
}
|
||||
|
||||
private static int GenerateAndShowCounter = 0;
|
||||
|
||||
public static void GeneratePdfAndShow(this IDocument document)
|
||||
{
|
||||
GenerateAndShowCounter++;
|
||||
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"QuestPDF Document {GenerateAndShowCounter}.pdf");
|
||||
document.GeneratePdf(filePath);
|
||||
OpenFileUsingDefaultProgram(filePath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region XPS
|
||||
@@ -50,32 +62,68 @@ namespace QuestPDF.Fluent
|
||||
DocumentGenerator.GenerateXps(stream, document);
|
||||
}
|
||||
|
||||
public static void GenerateXpsAndShow(this IDocument document)
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"QuestPDF Document.xps");
|
||||
document.GenerateXps(filePath);
|
||||
OpenFileUsingDefaultProgram(filePath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Images
|
||||
|
||||
public static IEnumerable<byte[]> GenerateImages(this IDocument document)
|
||||
{
|
||||
return DocumentGenerator.GenerateImages(document);
|
||||
return document.GenerateImages(ImageGenerationSettings.Default);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static IEnumerable<byte[]> GenerateImages(this IDocument document, ImageGenerationSettings settings)
|
||||
{
|
||||
return DocumentGenerator.GenerateImages(document, settings);
|
||||
}
|
||||
|
||||
/// <param name="filePath">Method should return fileName for given index</param>
|
||||
public static void GenerateImages(this IDocument document, Func<int, string> filePath)
|
||||
{
|
||||
document.GenerateImages(filePath, ImageGenerationSettings.Default);
|
||||
}
|
||||
|
||||
/// <param name="filePath">Method should return fileName for given index</param>
|
||||
public static void GenerateImages(this IDocument document, Func<int, string> filePath, ImageGenerationSettings settings)
|
||||
{
|
||||
var index = 0;
|
||||
|
||||
foreach (var imageData in document.GenerateImages())
|
||||
|
||||
foreach (var imageData in document.GenerateImages(settings))
|
||||
{
|
||||
var path = filePath(index);
|
||||
|
||||
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
|
||||
|
||||
File.WriteAllBytes(path, imageData);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static void OpenFileUsingDefaultProgram(string filePath)
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo(filePath)
|
||||
{
|
||||
UseShellExecute = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -7,62 +7,192 @@ using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
public class ImageDescriptor
|
||||
{
|
||||
private Elements.Image ImageElement { get; }
|
||||
private AspectRatio AspectRatioElement { get; }
|
||||
private float ImageAspectRatio { get; }
|
||||
|
||||
internal ImageDescriptor(Elements.Image imageElement, Elements.AspectRatio aspectRatioElement)
|
||||
{
|
||||
ImageElement = imageElement;
|
||||
AspectRatioElement = aspectRatioElement;
|
||||
|
||||
var imageSize = ImageElement.DocumentImage.Size;
|
||||
ImageAspectRatio = imageSize.Width / (float)imageSize.Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, the library will not attempt to resize the image to fit the target DPI, nor save it with target image quality.
|
||||
/// </summary>
|
||||
public ImageDescriptor UseOriginalImage(bool value = true)
|
||||
{
|
||||
ImageElement.UseOriginalImage = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
|
||||
/// A larger DPI would create a PDF that reflects the original intent with better fidelity, but it can make for larger PDF files too, which would use more memory while rendering, and it would be slower to be processed or sent online or to printer.
|
||||
/// When generating images, this parameter also controls the resolution of the generated content.
|
||||
/// Default value is 144.
|
||||
/// </summary>
|
||||
public ImageDescriptor WithRasterDpi(int dpi)
|
||||
{
|
||||
ImageElement.TargetDpi = dpi;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encoding quality controls the trade-off between size and quality.
|
||||
/// When the image is opaque, it will be encoded using the JPEG format with the selected quality setting.
|
||||
/// When the image contains an alpha channel, it is always encoded using the PNG format and this option is ignored.
|
||||
/// The default value is "very high quality".
|
||||
/// </summary>
|
||||
public ImageDescriptor WithCompressionQuality(ImageCompressionQuality quality)
|
||||
{
|
||||
ImageElement.CompressionQuality = quality;
|
||||
return this;
|
||||
}
|
||||
|
||||
#region Aspect Ratio
|
||||
|
||||
/// <summary>
|
||||
/// The image scales to take the entire available width. Default.
|
||||
/// </summary>
|
||||
public ImageDescriptor FitWidth()
|
||||
{
|
||||
return SetAspectRatio(AspectRatioOption.FitWidth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The images scales to take the entire available height. Good in conjunction with constraining elements.
|
||||
/// </summary>
|
||||
public ImageDescriptor FitHeight()
|
||||
{
|
||||
return SetAspectRatio(AspectRatioOption.FitHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the combination of both of the FitWidth and the FitHeight options.
|
||||
/// The element scales to occupy the entire available area while preserving its aspect ratio.
|
||||
/// This means that sometimes it occupies the entire width and sometimes the entire height.
|
||||
/// This is the safest option.
|
||||
/// </summary>
|
||||
public ImageDescriptor FitArea()
|
||||
{
|
||||
return SetAspectRatio(AspectRatioOption.FitArea);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The image resizes itself to occupy the entire available space.
|
||||
/// It does not preserve proportions.
|
||||
/// The image may look incorrectly scaled, and is not desired in most of the cases.
|
||||
/// </summary>
|
||||
public ImageDescriptor FitUnproportionally()
|
||||
{
|
||||
AspectRatioElement.Ratio = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
private ImageDescriptor SetAspectRatio(AspectRatioOption option)
|
||||
{
|
||||
AspectRatioElement.Ratio = ImageAspectRatio;
|
||||
AspectRatioElement.Option = option;
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static class ImageExtensions
|
||||
{
|
||||
public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
public static ImageDescriptor Image(this IContainer parent, byte[] imageData)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(imageData);
|
||||
parent.Image(image, scaling);
|
||||
var image = Infrastructure.Image.FromBinaryData(imageData).DisposeAfterDocumentGeneration();
|
||||
return parent.Image(image);
|
||||
}
|
||||
|
||||
public static void Image(this IContainer parent, string filePath, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
public static ImageDescriptor Image(this IContainer parent, string filePath)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(filePath);
|
||||
parent.Image(image, scaling);
|
||||
var image = Infrastructure.Image.FromFile(filePath).DisposeAfterDocumentGeneration();
|
||||
return parent.Image(image);
|
||||
}
|
||||
|
||||
public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
public static ImageDescriptor Image(this IContainer parent, Stream fileStream)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(fileStream);
|
||||
parent.Image(image, scaling);
|
||||
var image = Infrastructure.Image.FromStream(fileStream).DisposeAfterDocumentGeneration();
|
||||
return parent.Image(image);
|
||||
}
|
||||
|
||||
private static void Image(this IContainer parent, SKImage image, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
internal static ImageDescriptor Image(this IContainer parent, Infrastructure.Image image)
|
||||
{
|
||||
if (image == null)
|
||||
throw new DocumentComposeException("Cannot load or decode provided image.");
|
||||
|
||||
var imageElement = new Image
|
||||
var imageElement = new QuestPDF.Elements.Image
|
||||
{
|
||||
InternalImage = image
|
||||
DocumentImage = image
|
||||
};
|
||||
|
||||
if (scaling != ImageScaling.Resize)
|
||||
var aspectRationElement = new AspectRatio
|
||||
{
|
||||
var aspectRatio = image.Width / (float)image.Height;
|
||||
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()
|
||||
};
|
||||
}
|
||||
Child = imageElement
|
||||
};
|
||||
|
||||
parent.Element(aspectRationElement);
|
||||
return new ImageDescriptor(imageElement, aspectRationElement).FitWidth();
|
||||
}
|
||||
|
||||
public static void Image(this IContainer element, Func<Size, byte[]> imageSource)
|
||||
public static void Image(this IContainer element, GenerateDynamicImageDelegate dynamicImageSource)
|
||||
{
|
||||
element.Element(new DynamicImage
|
||||
{
|
||||
Source = imageSource
|
||||
Source = dynamicImageSource
|
||||
});
|
||||
}
|
||||
|
||||
#region Obsolete
|
||||
|
||||
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that takes the GenerateDynamicImageDelegate as an argument.")]
|
||||
public static void Image(this IContainer element, Func<Size, byte[]> imageSource)
|
||||
{
|
||||
element.Image((ImageSize x) => imageSource(new Size(x.Width, x.Height)));
|
||||
}
|
||||
|
||||
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
|
||||
public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling)
|
||||
{
|
||||
parent.Image(imageData).ApplyScaling(scaling);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
|
||||
public static void Image(this IContainer parent, string filePath, ImageScaling scaling)
|
||||
{
|
||||
parent.Image(filePath).ApplyScaling(scaling);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
|
||||
public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling)
|
||||
{
|
||||
parent.Image(fileStream).ApplyScaling(scaling);
|
||||
}
|
||||
|
||||
internal static void ApplyScaling(this ImageDescriptor descriptor, ImageScaling scaling)
|
||||
{
|
||||
if (scaling == ImageScaling.Resize)
|
||||
descriptor.FitUnproportionally();
|
||||
|
||||
else if (scaling == ImageScaling.FitWidth)
|
||||
descriptor.FitWidth();
|
||||
|
||||
else if (scaling == ImageScaling.FitHeight)
|
||||
descriptor.FitHeight();
|
||||
|
||||
else if (scaling == ImageScaling.FitArea)
|
||||
descriptor.FitArea();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
private Action<IDocumentContainer> ContentSource { get; }
|
||||
private DocumentMetadata Metadata { get; set; } = DocumentMetadata.Default;
|
||||
private DocumentSettings Settings { get; set; } = DocumentSettings.Default;
|
||||
|
||||
private Document(Action<IDocumentContainer> contentSource)
|
||||
{
|
||||
@@ -25,9 +26,16 @@ namespace QuestPDF.Fluent
|
||||
return this;
|
||||
}
|
||||
|
||||
public Document WithSettings(DocumentSettings settings)
|
||||
{
|
||||
Settings = settings ?? Settings;
|
||||
return this;
|
||||
}
|
||||
|
||||
#region IDocument
|
||||
|
||||
public DocumentMetadata GetMetadata() => Metadata;
|
||||
public DocumentSettings GetSettings() => Settings;
|
||||
public void Compose(IDocumentContainer container) => ContentSource(container);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -26,15 +26,7 @@ namespace QuestPDF.Helpers
|
||||
|
||||
static bool IsColorValid(string color)
|
||||
{
|
||||
try
|
||||
{
|
||||
SKColor.Parse(color);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return SKColor.TryParse(color, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Helpers
|
||||
{
|
||||
@@ -60,5 +61,52 @@ namespace QuestPDF.Helpers
|
||||
{
|
||||
return size.Width < 0f || size.Height < 0f;
|
||||
}
|
||||
|
||||
internal static int ToQualityValue(this ImageCompressionQuality quality)
|
||||
{
|
||||
return quality switch
|
||||
{
|
||||
ImageCompressionQuality.Best => 100,
|
||||
ImageCompressionQuality.VeryHigh => 90,
|
||||
ImageCompressionQuality.High => 75,
|
||||
ImageCompressionQuality.Medium => 50,
|
||||
ImageCompressionQuality.Low => 25,
|
||||
ImageCompressionQuality.VeryLow => 10,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(quality), quality, null)
|
||||
};
|
||||
}
|
||||
|
||||
internal static SKImage CompressImage(this SKImage image, ImageCompressionQuality compressionQuality)
|
||||
{
|
||||
var targetFormat = image.Info.IsOpaque
|
||||
? SKEncodedImageFormat.Jpeg
|
||||
: SKEncodedImageFormat.Png;
|
||||
|
||||
if (targetFormat == SKEncodedImageFormat.Png)
|
||||
compressionQuality = ImageCompressionQuality.Best;
|
||||
|
||||
var data = image.Encode(targetFormat, compressionQuality.ToQualityValue());
|
||||
return SKImage.FromEncodedData(data);
|
||||
}
|
||||
|
||||
internal static SKImage ResizeAndCompressImage(this SKImage image, ImageSize targetResolution, ImageCompressionQuality compressionQuality)
|
||||
{
|
||||
if (image.Width == targetResolution.Width && image.Height == targetResolution.Height)
|
||||
return CompressImage(image, compressionQuality);
|
||||
|
||||
var imageInfo = new SKImageInfo(targetResolution.Width, targetResolution.Height, image.Info.ColorType, image.Info.AlphaType, image.Info.ColorSpace);
|
||||
|
||||
using var resultImage = SKImage.Create(imageInfo);
|
||||
image.ScalePixels(resultImage.PeekPixels(), SKFilterQuality.Medium);
|
||||
|
||||
return CompressImage(resultImage, compressionQuality);
|
||||
}
|
||||
|
||||
internal static SKImage GetImageWithSmallerSize(SKImage one, SKImage second)
|
||||
{
|
||||
return one.EncodedData.Size < second.EncodedData.Size
|
||||
? one
|
||||
: second;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,10 +213,10 @@ namespace QuestPDF.Helpers
|
||||
|
||||
public static byte[] Image(int width, int height)
|
||||
{
|
||||
return Image(new Size(width, height));
|
||||
return Image(new ImageSize(width, height));
|
||||
}
|
||||
|
||||
public static byte[] Image(Size size)
|
||||
public static byte[] Image(ImageSize size)
|
||||
{
|
||||
// shuffle corner positions
|
||||
var targetPositions = new[]
|
||||
@@ -239,7 +239,7 @@ namespace QuestPDF.Helpers
|
||||
.ToArray();
|
||||
|
||||
// create image with white background
|
||||
var imageInfo = new SKImageInfo((int)size.Width, (int)size.Height);
|
||||
var imageInfo = new SKImageInfo(size.Width, size.Height);
|
||||
using var surface = SKSurface.Create(imageInfo);
|
||||
|
||||
using var backgroundPaint = new SKPaint
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
internal enum ContentDirection
|
||||
public enum ContentDirection
|
||||
{
|
||||
LeftToRight,
|
||||
RightToLeft
|
||||
|
||||
+16
-8
@@ -1,14 +1,9 @@
|
||||
using System;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public class DocumentMetadata
|
||||
{
|
||||
public int ImageQuality { get; set; } = 101;
|
||||
public int RasterDpi { get; set; } = 72;
|
||||
public bool PdfA { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
public string? Author { get; set; }
|
||||
public string? Subject { get; set; }
|
||||
@@ -19,6 +14,10 @@ namespace QuestPDF.Drawing
|
||||
public DateTime CreationDate { get; set; } = DateTime.Now;
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
public static DocumentMetadata Default => new DocumentMetadata();
|
||||
|
||||
#region Deprecated properties
|
||||
|
||||
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.DocumentLayoutExceptionThreshold static property.")]
|
||||
public int DocumentLayoutExceptionThreshold
|
||||
{
|
||||
@@ -39,7 +38,16 @@ namespace QuestPDF.Drawing
|
||||
get => Settings.EnableDebugging;
|
||||
set => Settings.EnableDebugging = value;
|
||||
}
|
||||
|
||||
public static DocumentMetadata Default => new DocumentMetadata();
|
||||
|
||||
[Obsolete("This API has been moved since version 2023.5. Please use the QuestPDF.Infrastructure.DocumentSettings API.")]
|
||||
public int? ImageQuality { get; set; }
|
||||
|
||||
[Obsolete("This API has been moved since version 2023.5. Please use the QuestPDF.Infrastructure.DocumentSettings API.")]
|
||||
public int? RasterDpi { get; set; }
|
||||
|
||||
[Obsolete("This API has been moved since version 2023.5. Please use the QuestPDF.Infrastructure.DocumentSettings API.")]
|
||||
public bool? PdfA { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public class DocumentSettings
|
||||
{
|
||||
public const int DefaultRasterDpi = 72;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not make the document PDF/A-2b conformant.
|
||||
/// If true, include XMP metadata, a document UUID, and sRGB output intent information.
|
||||
/// This adds length to the document and makes it non-reproducable, but are necessary features for PDF/A-2b conformance.
|
||||
/// </summary>
|
||||
public bool PdfA { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Encoding quality controls the trade-off between size and quality.
|
||||
/// When the image is opaque, it will be encoded using the JPEG format with the selected quality setting.
|
||||
/// When the image contains an alpha channel, it is always encoded using the PNG format and this option is ignored.
|
||||
/// The default value is "high quality".
|
||||
/// </summary>
|
||||
public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.VeryHigh;
|
||||
|
||||
/// <summary>
|
||||
/// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
|
||||
/// A larger DPI would create a PDF that reflects the original intent with better fidelity, but it can make for larger PDF files too, which would use more memory while rendering, and it would be slower to be processed or sent online or to printer.
|
||||
/// When generating images, this parameter also controls the resolution of the generated content.
|
||||
/// Default value is 144.
|
||||
/// </summary>
|
||||
public int ImageRasterDpi { get; set; } = DefaultRasterDpi * 2;
|
||||
|
||||
public ContentDirection ContentDirection { get; set; } = ContentDirection.LeftToRight;
|
||||
|
||||
public static DocumentSettings Default => new DocumentSettings();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,14 @@ namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public interface IDocument
|
||||
{
|
||||
#if NETCOREAPP3_0_OR_GREATER
|
||||
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
|
||||
public DocumentSettings GetSettings() => DocumentSettings.Default;
|
||||
#else
|
||||
DocumentMetadata GetMetadata();
|
||||
DocumentSettings GetSettings();
|
||||
#endif
|
||||
|
||||
void Compose(IDocumentContainer container);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Helpers;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
internal record GetImageVersionRequest
|
||||
{
|
||||
internal ImageSize Resolution { get; set; }
|
||||
internal ImageCompressionQuality CompressionQuality { get; set; }
|
||||
}
|
||||
|
||||
public class Image : IDisposable
|
||||
{
|
||||
internal SKImage SkImage { get; }
|
||||
internal ImageSize Size { get; }
|
||||
internal bool IsDocumentScoped { get; set; }
|
||||
|
||||
internal LinkedList<(GetImageVersionRequest request, SKImage image)> ScaledImageCache { get; } = new();
|
||||
|
||||
private Image(SKImage image)
|
||||
{
|
||||
SkImage = image;
|
||||
Size = new ImageSize(image.Width, image.Height);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SkImage.Dispose();
|
||||
|
||||
foreach (var cacheKey in ScaledImageCache)
|
||||
cacheKey.image.Dispose();
|
||||
}
|
||||
|
||||
#region Scaling Image
|
||||
|
||||
internal SKImage GetVersionOfSize(GetImageVersionRequest request)
|
||||
{
|
||||
foreach (var cacheKey in ScaledImageCache)
|
||||
{
|
||||
if (cacheKey.request == request)
|
||||
return cacheKey.image;
|
||||
}
|
||||
|
||||
var result = SkImage.ResizeAndCompressImage(request.Resolution, request.CompressionQuality);
|
||||
ScaledImageCache.AddLast((request, result));
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public constructors
|
||||
|
||||
internal static Image FromSkImage(SKImage image)
|
||||
{
|
||||
return CreateImage(image);
|
||||
}
|
||||
|
||||
public static Image FromBinaryData(byte[] imageData)
|
||||
{
|
||||
return CreateImage(SKImage.FromEncodedData(imageData));
|
||||
}
|
||||
|
||||
public static Image FromFile(string filePath)
|
||||
{
|
||||
return CreateImage(SKImage.FromEncodedData(filePath));
|
||||
}
|
||||
|
||||
public static Image FromStream(Stream fileStream)
|
||||
{
|
||||
return CreateImage(SKImage.FromEncodedData(fileStream));
|
||||
}
|
||||
|
||||
private static Image CreateImage(SKImage? image)
|
||||
{
|
||||
if (image == null)
|
||||
throw new DocumentComposeException("Cannot load or decode provided image.");
|
||||
|
||||
return new Image(image);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal Image DisposeAfterDocumentGeneration()
|
||||
{
|
||||
IsDocumentScoped = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public enum ImageCompressionQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 100 out of 100
|
||||
/// </summary>
|
||||
Best,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 90 out of 100
|
||||
/// </summary>
|
||||
VeryHigh,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 75 out of 100
|
||||
/// </summary>
|
||||
High,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 50 out of 100
|
||||
/// </summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 25 out of 100
|
||||
/// </summary>
|
||||
Low,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 10 out of 100
|
||||
/// </summary>
|
||||
VeryLow
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public class ImageGenerationSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The file format used to encode the image(s).
|
||||
/// </summary>
|
||||
public SKEncodedImageFormat Format { get; set; } = SKEncodedImageFormat.Png;
|
||||
|
||||
/// <summary>
|
||||
/// The quality level to use for the image(s). This is in the range from 0-100.
|
||||
/// </summary>
|
||||
public int Quality { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
|
||||
/// A larger DPI would create a PDF that reflects the original intent with better fidelity, but it can make for larger PDF files too, which would use more memory while rendering, and it would be slower to be processed or sent online or to printer.
|
||||
/// When generating images, this parameter also controls the resolution of the generated content.
|
||||
/// Default value is 144.
|
||||
/// </summary>
|
||||
public int RasterDpi { get; set; } = 144;
|
||||
|
||||
|
||||
public static ImageGenerationSettings Default => new ImageGenerationSettings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public struct ImageSize
|
||||
{
|
||||
public readonly int Width;
|
||||
public readonly int Height;
|
||||
|
||||
public ImageSize(int width, int height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@ namespace QuestPDF.Previewer
|
||||
{
|
||||
return DocumentMetadata.Default;
|
||||
}
|
||||
|
||||
public DocumentSettings GetSettings()
|
||||
{
|
||||
return DocumentSettings.Default;
|
||||
}
|
||||
|
||||
public void Compose(IDocumentContainer document)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace QuestPDF.Previewer
|
||||
public event Action? OnPreviewerStopped;
|
||||
|
||||
private const int RequiredPreviewerVersionMajor = 2023;
|
||||
private const int RequiredPreviewerVersionMinor = 4;
|
||||
private const int RequiredPreviewerVersionMinor = 5;
|
||||
|
||||
public PreviewerService(int port)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF</PackageId>
|
||||
<Version>2023.4.1</Version>
|
||||
<Version>2023.5.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. Easily generate PDF reports, invoices, exports, etc.</PackageDescription>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<LangVersion>9</LangVersion>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
Version 2023.4.0
|
||||
|
||||
This release does not contain any features or quality improvements.
|
||||
Its purpose is to mark the QuestPDF shift towards the dual-licensing model.
|
||||
Most users are not affected by this change.
|
||||
Please visit the https://www.questpdf.com/pricing.html webpage for more information.
|
||||
Version 2023.5.0
|
||||
- Simplified development loop by introducing cross-platform methods: GeneratePdfAndShow() and GenerateXpsAndShow()
|
||||
- New shared image API: the ability to define a single image resource that is used in multiple places in the document without increasing its size
|
||||
- New DocumentSettings API: target image raster DPI - now, the library automatically resizes all images to achieve desired DPI (dots-per-inch) resolution. It allows for minimizing output file size
|
||||
- New DocumentSettings API: target image compression quality - the ability to specify the balance between size and quality for images in the document. It allows for minimizing output file size
|
||||
- Refactoring: moved the PdfA setting from the DocumentMetadata class to the DocumentSettings class
|
||||
- Improved Image API by providing additional FluentA API methods
|
||||
- Improvement: the GenerateImage element now provides the expected image resolution, abstracting away the physical area size and target image DPI
|
||||
|
||||
Reference in New Issue
Block a user