Compare commits

..

108 Commits

Author SHA1 Message Date
MarcinZiabek 0ebf2833af Updated release notes 2022-05-06 21:17:16 +02:00
MarcinZiabek 25e6d9fbca Adjusted library versions 2022-05-06 21:09:16 +02:00
Marcin Ziąbek f80149792d Merge pull request #178 from jnyrup/Dispose
Explicit disposal of disposables
2022-05-06 20:59:35 +02:00
Marcin Ziąbek 2beee1d22e Update PreviewerService.cs 2022-05-06 20:59:06 +02:00
MarcinZiabek 1af7c0a144 Adjusted code related to subscript/superscript 2022-05-06 20:55:18 +02:00
Marcin Ziąbek eeca67e5bc Merge pull request #179 from Bebo-Maker/main
Implement subscript/superscript text
2022-05-06 00:09:31 +02:00
MarcinZiabek 4da3ef5dfa Fixed breaking long words 2022-05-05 23:52:17 +02:00
MarcinZiabek f3d4151273 Improved performance of the table rendering algorithm (square to linear complexity) 2022-05-05 23:20:23 +02:00
MarcinZiabek a2629575e8 Dynamic: simplified container composition process 2022-04-27 09:50:17 +02:00
MarcinZiabek c8824021cc Dynamic: generalized dynamic element size measurement 2022-04-27 09:24:44 +02:00
MarcinZiabek e1101d870d Dynamic: improve state management 2022-04-27 06:56:13 +02:00
MarcinZiabek 152304b467 Dynamic: improved TextStyle handling 2022-04-26 01:11:40 +02:00
MarcinZiabek 8a1be90c4f Ported dynamic component implementation prototype 2022-04-26 00:36:50 +02:00
MarcinZiabek bc8a9e5dc5 Fixed mutating text style for page-related text items 2022-04-23 23:13:38 +02:00
Bebo-Maker c63b0658f3 Ignore FontPosition when calculating font metrics. 2022-04-12 19:14:00 +02:00
Bebo-Maker 82cd1d7835 Add example for superscript/subscript. 2022-04-12 18:49:34 +02:00
Bebo-Maker 3ffab6e933 Implement superscript and subscript. 2022-04-12 18:48:28 +02:00
Marcin Ziąbek e4d55e0298 Merge pull request #177 from jnyrup/ArgumentException
Use `paramName` overload of `ArgumentException`
2022-04-11 22:03:40 +02:00
Jonas Nyrup a6f4fd7f49 explicit disposal 2022-04-11 21:36:50 +02:00
Jonas Nyrup 048a4748b2 Use paramName overload of ArgumentException 2022-04-11 21:33:33 +02:00
Marcin Ziąbek 8cc2fb8696 Merge pull request #169 from fredericoregateiro/AvaloniaCompiledBindings
Enable compiled bindings on Previewer
2022-04-08 22:17:41 +02:00
MarcinZiabek 8e7b528cd9 Updated install command in readme 2022-04-08 22:15:12 +02:00
MarcinZiabek 26193313b8 Previewer: hide "Show in default browser" button when no document available 2022-04-08 22:14:19 +02:00
MarcinZiabek 555825e8be Previewer: throwing exception when cannot connect with frontend 2022-04-08 22:14:19 +02:00
Frederico Regateiro ddde8e7517 enable compiled bindings on PreviewerWindow 2022-04-06 14:08:53 +01:00
Marcin Ziąbek d2c82b5988 Update readme.md 2022-04-05 14:54:33 +02:00
Marcin Ziąbek d82aae95b5 Update readme.md 2022-04-05 14:34:00 +02:00
Marcin Ziąbek 5c11a2ab6a Update readme.md 2022-04-05 14:30:51 +02:00
MarcinZiabek 03e3a80fd9 Fixed logo resource for QuestPDF.Previwer 2022-04-04 23:26:16 +02:00
MarcinZiabek 4692c5ce62 Fixed QuestPDF.Previewer build 2022-04-04 23:24:02 +02:00
MarcinZiabek 919f916219 2022.4 release 2022-04-04 23:20:22 +02:00
MarcinZiabek 7a32f774a1 Fixed spelling 2022-04-04 00:38:58 +02:00
MarcinZiabek fba71e6392 Previewer: stop debugging session when previewer is closed 2022-04-04 00:19:36 +02:00
MarcinZiabek e27585a504 Fixed previewer: first document refresh sometimes does not work 2022-04-02 00:23:58 +02:00
MarcinZiabek 9be497ee41 Fixed automatically starting previewer application on custom port 2022-04-01 19:26:54 +02:00
MarcinZiabek 0fc0db55d2 Better button style path in previewer 2022-03-31 23:14:00 +02:00
MarcinZiabek 9e7a1ed49d Previewer improvements 2022-03-31 23:11:34 +02:00
MarcinZiabek c137bcfcde Improved look and feel 2022-03-31 18:33:34 +02:00
MarcinZiabek b80b3414d8 Added http-based previewer 2022-03-31 15:44:28 +02:00
MarcinZiabek 5f7cee3420 Removed old previewer implementation 2022-03-30 23:38:51 +02:00
MarcinZiabek 435c42ab9e Changed previewer communication layer to http 2022-03-30 23:37:37 +02:00
MarcinZiabek 7b4923b45d Merge remote-tracking branch 'origin/previewer' 2022-03-27 20:15:02 +02:00
MarcinZiabek 8732b7e50b Renaming 2022-03-27 20:12:22 +02:00
MarcinZiabek 6b31322f92 Fixed preview button labels 2022-03-27 17:29:50 +02:00
MarcinZiabek 6155d21207 Previewer: added buttons for documentation and project sponsoring 2022-03-27 14:49:50 +02:00
Marcin Ziąbek a0909eb057 Create FUNDING.yml 2022-03-26 21:06:44 +01:00
MarcinZiabek 11ff53946d Introduced new line breaking algorithm: break anywhere 2022-03-26 03:34:31 +01:00
MarcinZiabek 0711bca1e1 Improved respecting spaces at the beginning and end of text block, fixed breaking long words that do not fit in line 2022-03-26 02:58:55 +01:00
MarcinZiabek 297ca48c70 Code cleanup 2022-03-24 23:09:09 +01:00
Marcin Ziąbek d2601e9e5a Merge pull request #157 from Bebo-Maker/previewer
Previewer - ViewModel for PreviewerWindow
2022-03-24 22:20:28 +01:00
Bebo-Maker 421a926780 Fix vertical scroll bar visibility. 2022-03-24 22:05:49 +01:00
Bebo-Maker 307759d1b3 Remove StyledProperties and rework bindings. 2022-03-24 22:01:10 +01:00
Bebo-Maker 00bcb2c25b Add ViewModel for PreviewerWindow. 2022-03-24 21:59:55 +01:00
MarcinZiabek e430484e85 Fixed zooming out 2022-03-24 13:54:38 +01:00
MarcinZiabek d24d887a3a Previewer: simplified translate operations 2022-03-24 13:35:02 +01:00
MarcinZiabek 404914ea7e Previewer: added button to show generated PDF 2022-03-24 01:02:57 +01:00
Marcin Ziąbek 545b76fd40 Merge pull request #155 from Bebo-Maker/previewer
Previewer - Fix multiple small issues
2022-03-24 00:04:34 +01:00
Marcin Ziąbek c801894ec4 Merge branch 'previewer' into previewer 2022-03-24 00:04:28 +01:00
MarcinZiabek c21744e7de Fixed scrolling math 2022-03-23 23:31:27 +01:00
Bebo-Maker 734d116bfe Fix scroll viewer visibility. 2022-03-23 12:58:58 +01:00
Bebo-Maker 0abb67b748 Unregister HotReload handler when window is closed 2022-03-23 09:22:46 +01:00
Bebo-Maker 828bb86737 Fix MaxWidth calculation when pages are empty. 2022-03-23 08:35:53 +01:00
Bebo-Maker 040c47e017 Fix issue when showing previewer multiple times.
Previously when calling ShowPreviewer  in the same process multiple times lead to an Exception inside Avalonia.
2022-03-23 08:35:28 +01:00
MarcinZiabek ff95fe287a Added scrollbar interactions 2022-03-23 01:10:15 +01:00
MarcinZiabek 47e9a49fe5 Fixed scroll, more code refactoring 2022-03-21 01:33:33 +01:00
MarcinZiabek b2826a06b8 Code refactorization + cleanup 2022-03-21 01:16:26 +01:00
MarcinZiabek 321cba32ce Implemented basic canvas interactions 2022-03-21 01:02:53 +01:00
MarcinZiabek 26fdbb714d Simplified rendering operation 2022-03-20 01:53:51 +01:00
Marcin Ziąbek f0cec8988a Merge pull request #147 from Bebo-Maker/document-previewer
Document previewer
2022-03-20 00:48:50 +01:00
Bebo-Maker ea2102fe4c WIP Implemented virtualization of pages.
Render pages into seperate SKPictures
2022-03-19 17:26:58 +01:00
Bebo-Maker 6cca3ed057 Refactor custom drawing operation. 2022-03-17 22:32:37 +01:00
Bebo-Maker 9afe4ec104 Add button for generating pdf in previewer. 2022-03-17 20:03:20 +01:00
Bebo-Maker 846679007f Enable dark mode for previewer and cleanup UI. 2022-03-17 19:35:46 +01:00
Bebo-Maker 0946607154 Add table to example. 2022-03-17 12:05:27 +01:00
Bebo-Maker d5f86e0d70 Fixed rendering issue where content was clipped. 2022-03-17 11:44:40 +01:00
Bebo-Maker 346513ee85 Optimize document rendering.
We dont need to render the document two times anymore.
2022-03-17 10:34:00 +01:00
Bebo-Maker d18ee5f282 Allow intersection after first RenderPass. 2022-03-17 10:32:41 +01:00
Bebo-Maker a85bc0abc1 Add example for using document previewer. 2022-03-16 22:54:03 +01:00
Bebo-Maker 82e39df572 Enable SKPaint.IsAntialias in FontManager. 2022-03-16 22:48:30 +01:00
Bebo-Maker 1311b6db32 Add PreviewerApp and provide extensions for displaying previewer. 2022-03-16 22:48:07 +01:00
Bebo-Maker b5645bfbd5 Implement document preview. 2022-03-16 22:47:05 +01:00
Bebo-Maker 8217b3b54a Implement HotReloadManager. 2022-03-16 22:46:21 +01:00
Bebo-Maker 726759f47a Add size tracking canvas. 2022-03-16 22:45:57 +01:00
Bebo-Maker bbd9c3379e Change access modifier of RenderDocument to internal. 2022-03-16 22:45:39 +01:00
Bebo-Maker 697c5e238d Add InternalsVisibleTo for QuestPDF.Previewer in QuestPDF AssemblyInfo. 2022-03-16 22:45:13 +01:00
Bebo-Maker 21a92be87e Add .editorconfig . 2022-03-16 22:44:45 +01:00
Bebo-Maker b0cca1ff48 Add QuestPDF.Previewer and QuestPDF.Previewer.Examples 2022-03-16 22:42:16 +01:00
Marcin Ziąbek 78c5049eda Update readme.md 2022-03-15 23:23:18 +01:00
MarcinZiabek 3d0ca07b43 Release 2022.3.1 2022-03-15 13:06:09 +01:00
MarcinZiabek a67fa754af Fixed NullReferenceException when the text is null 2022-03-15 11:55:59 +01:00
MarcinZiabek 208235ad1e Updated description 2022-03-14 13:59:26 +01:00
Marcin Ziąbek f24416474a Update readme.md 2022-03-14 13:49:21 +01:00
MarcinZiabek 01c2e8d3f5 Updated documentation. Release 2022.03 2022-03-14 13:17:33 +01:00
MarcinZiabek b3cef238ec Improved page color API 2022-03-13 22:47:17 +01:00
Marcin Ziąbek aede62c135 Update readme.md 2022-03-12 23:04:11 +01:00
MarcinZiabek db9db95dcc Fixed build 2022-03-12 19:20:01 +01:00
MarcinZiabek 92263685d7 Merge branch '2022.03'
# Conflicts:
#	QuestPDF.Examples/TextExamples.cs
#	QuestPDF/QuestPDF.csproj
2022-03-12 19:17:38 +01:00
MarcinZiabek ba4be521aa 2022.2.7 2022-03-12 19:01:39 +01:00
Marcin Ziąbek f6967dfe8b Update readme.md 2022-03-12 16:42:26 +01:00
MarcinZiabek ff3dfe7831 Fixed logo size 2022-03-12 16:39:33 +01:00
MarcinZiabek 5a1990d857 Added more shields 2022-03-12 16:38:03 +01:00
Marcin Ziąbek 9c64a6bc8d Merge pull request #128 from loxsmoke/main
Add nuget badges to readme.md file
2022-03-12 16:06:04 +01:00
MarcinZiabek 5402e5fedc Fixed text rendering issues 2022-03-12 12:17:00 +01:00
MarcinZiabek fdf8e3eb1a Fixed text rendering corner case: measuring span with space at the end of the line may overflow available space 2022-03-08 18:33:34 +01:00
LoxSmoke 456c7a735b Update readme.md 2022-02-24 19:55:36 -08:00
LoxSmoke 5bf2a9a3d3 Nuget badges in readme file 2022-02-24 19:53:51 -08:00
MarcinZiabek 69e268733c 2022.2.5 2022-02-18 23:31:53 +01:00
MarcinZiabek 657bbe2aac 2022.2.4 2022-02-18 21:48:57 +01:00
68 changed files with 2574 additions and 472 deletions
+131
View File
@@ -0,0 +1,131 @@
# Rules in this file were initially inferred by Visual Studio IntelliCode from the D:\GithubRepos\QuestPDF codebase based on best match to current usage at 16.03.2022
# You can modify the rules from these initially generated values to suit your own policies
# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
[*.cs]
#Core editorconfig formatting - indentation
#use soft tabs (spaces) for indentation
indent_style = space
#Formatting - new line options
#place catch statements on a new line
csharp_new_line_before_catch = true
#place else statements on a new line
csharp_new_line_before_else = true
#require members of anonymous types to be on separate lines
csharp_new_line_before_members_in_anonymous_types = true
#require members of object intializers to be on separate lines
csharp_new_line_before_members_in_object_initializers = true
#require braces to be on a new line for object_collection_array_initializers, methods, anonymous_types, control_blocks, types, and lambdas (also known as "Allman" style)
csharp_new_line_before_open_brace =all
#Formatting - organize using options
#sort System.* using directives alphabetically, and place them before other usings
dotnet_sort_system_directives_first = true
#Formatting - spacing options
#require NO space between a cast and the value
csharp_space_after_cast = false
#require a space before the colon for bases or interfaces in a type declaration
csharp_space_after_colon_in_inheritance_clause = true
#require a space after a keyword in a control flow statement such as a for loop
csharp_space_after_keywords_in_control_flow_statements = true
#require a space before the colon for bases or interfaces in a type declaration
csharp_space_before_colon_in_inheritance_clause = true
#remove space within empty argument list parentheses
csharp_space_between_method_call_empty_parameter_list_parentheses = false
#remove space between method call name and opening parenthesis
csharp_space_between_method_call_name_and_opening_parenthesis = false
#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call
csharp_space_between_method_call_parameter_list_parentheses = false
#remove space within empty parameter list parentheses for a method declaration
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list.
csharp_space_between_method_declaration_parameter_list_parentheses = false
#Formatting - wrapping options
#leave code block on single line
csharp_preserve_single_line_blocks = true
#Style - Code block preferences
#prefer no curly braces if allowed
csharp_prefer_braces = false:suggestion
#Style - expression bodied member options
#prefer block bodies for constructors
csharp_style_expression_bodied_constructors = false:suggestion
#prefer block bodies for methods
csharp_style_expression_bodied_methods = false:suggestion
#prefer expression-bodied members for properties
csharp_style_expression_bodied_properties = true:suggestion
#Style - expression level options
#prefer out variables to be declared inline in the argument list of a method call when possible
csharp_style_inlined_variable_declaration = true:suggestion
#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them
dotnet_style_predefined_type_for_member_access = true:suggestion
#Style - Expression-level preferences
#prefer objects to be initialized using object initializers when possible
dotnet_style_object_initializer = true:suggestion
#prefer inferred anonymous type member names
dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion
#prefer inferred tuple element names
dotnet_style_prefer_inferred_tuple_names = true:suggestion
#Style - implicit and explicit types
#prefer var over explicit type in all cases, unless overridden by another code style rule
csharp_style_var_elsewhere = true:suggestion
#prefer var is used to declare variables with built-in system types such as int
csharp_style_var_for_built_in_types = true:suggestion
#prefer var when the type is already mentioned on the right-hand side of a declaration expression
csharp_style_var_when_type_is_apparent = true:suggestion
#Style - language keyword and framework type options
#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
#Style - Miscellaneous preferences
#prefer local functions over anonymous functions
csharp_style_pattern_local_over_anonymous_function = true:suggestion
#Style - modifier options
#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods.
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
#Style - Modifier preferences
#when this rule is set to a list of modifiers, prefer the specified ordering.
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
#Style - Pattern matching
#prefer pattern matching instead of is expression with type casts
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
#Style - qualification options
#prefer fields not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_field = false:suggestion
#prefer methods not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_method = false:suggestion
#prefer properties not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_property = false:suggestion
[*.{cs,vb}]
tab_width=4
indent_size=4
+1
View File
@@ -0,0 +1 @@
github: QuestPDF
+1 -1
View File
@@ -16,7 +16,7 @@ namespace QuestPDF.Examples
.PageSize(PageSizes.A4)
.ProducePdf()
.ShowResults()
.Render(x => GenerateStructure(x, 16));
.Render(x => GenerateStructure(x, 12));
}
private void GenerateStructure(IContainer container, int level)
+1 -1
View File
@@ -24,7 +24,7 @@ namespace QuestPDF.Examples
page.Margin(20);
page.Size(PageSizes.A4);
page.Background(Colors.White);
page.PageColor(Colors.White);
page.Content().Column(column =>
{
@@ -24,7 +24,7 @@ namespace QuestPDF.Examples
{
page.Size(PageSizes.A6);
page.Margin(5);
page.Background(Colors.White);
page.PageColor(Colors.White);
page.Header().Column(column =>
{
+185
View File
@@ -0,0 +1,185 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class OrderItem
{
public string ItemName { get; set; } = Placeholders.Label();
public int Price { get; set; } = Placeholders.Random.Next(1, 11) * 10;
public int Count { get; set; } = Placeholders.Random.Next(1, 11);
}
public struct OrdersTableState
{
public int ShownItemsCount { get; set; }
}
public class OrdersTable : IDynamicComponent<OrdersTableState>
{
private ICollection<OrderItem> Items { get; }
public OrdersTableState State { get; set; }
public OrdersTable(ICollection<OrderItem> items)
{
Items = items;
State = new OrdersTableState
{
ShownItemsCount = 0
};
}
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var header = ComposeHeader(context);
var sampleFooter = ComposeFooter(context, Enumerable.Empty<OrderItem>());
var decorationHeight = header.Size.Height + sampleFooter.Size.Height;
var rows = GetItemsForPage(context, decorationHeight).ToList();
var footer = ComposeFooter(context, rows.Select(x => x.Item));
var content = context.CreateElement(container =>
{
container.MinimalBox().Decoration(decoration =>
{
decoration.Header().Element(header);
decoration.Content().Box().Stack(stack =>
{
foreach (var row in rows)
stack.Item().Element(row.Element);
});
decoration.Footer().Element(footer);
});
});
State = new OrdersTableState
{
ShownItemsCount = State.ShownItemsCount + rows.Count
};
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = State.ShownItemsCount + rows.Count < Items.Count
};
}
private IDynamicElement ComposeHeader(DynamicContext context)
{
return context.CreateElement(element =>
{
element
.Width(context.AvailableSize.Width)
.BorderBottom(1)
.BorderColor(Colors.Grey.Darken2)
.Padding(5)
.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);
});
});
}
private IDynamicElement ComposeFooter(DynamicContext context, IEnumerable<OrderItem> items)
{
var total = items.Sum(x => x.Count * x.Price);
return context.CreateElement(element =>
{
element
.Width(context.AvailableSize.Width)
.Padding(5)
.AlignRight()
.Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold());
});
}
private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight)
{
var totalHeight = decorationHeight;
foreach (var index in Enumerable.Range(State.ShownItemsCount, Items.Count - State.ShownItemsCount))
{
var item = Items.ElementAt(index);
var element = context.CreateElement(content =>
{
content
.Width(context.AvailableSize.Width)
.BorderBottom(1)
.BorderColor(Colors.Grey.Lighten2)
.Padding(5)
.Row(row =>
{
row.ConstantItem(30).Text(index + 1);
row.RelativeItem().Text(item.ItemName);
row.ConstantItem(50).AlignRight().Text(item.Count);
row.ConstantItem(50).AlignRight().Text($"{item.Price}$");
row.ConstantItem(50).AlignRight().Text($"{item.Count*item.Price}$");
});
});
var elementHeight = element.Size.Height;
if (totalHeight + elementHeight > context.AvailableSize.Height)
break;
totalHeight += elementHeight;
yield return (item, element);
}
}
}
public static class DynamicExamples
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.ShowResults()
.Render(container =>
{
var items = Enumerable.Range(0, 25).Select(x => new OrderItem()).ToList();
container
.Background(Colors.White)
.Padding(25)
.Decoration(decoration =>
{
decoration
.Header()
.PaddingBottom(5)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2).FontSize(16));
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" of ");
text.TotalPages();
});
decoration
.Content()
.Dynamic(new OrdersTable(items));
});
});
}
}
}
+1 -1
View File
@@ -742,7 +742,7 @@ namespace QuestPDF.Examples
{
container
.Padding(25)
.PaddingLeft(75)
.PaddingLeft(50)
.Column(column =>
{
column.Item().Width(300).Height(150).Background(Colors.Blue.Lighten4);
+4 -2
View File
@@ -23,6 +23,8 @@ namespace QuestPDF.Examples.Engine
private bool ApplyCaching { get; set; }
private bool ApplyDebugging { get; set; }
private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images;
private bool ShowingResultsEnabled = true;
private RenderingTest()
{
@@ -114,7 +116,7 @@ namespace QuestPDF.Examples.Engine
Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
document.GenerateImages(fileNameSchema);
if (ShowResult)
if (ShowResult && ShowingResultsEnabled)
Process.Start("explorer", fileNameSchema(0));
}
@@ -123,7 +125,7 @@ namespace QuestPDF.Examples.Engine
var fileName = $"{FileNamePrefix}.pdf";
document.GeneratePdf(fileName);
if (ShowResult)
if (ShowResult && ShowingResultsEnabled)
Process.Start("explorer", fileName);
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ namespace QuestPDF.Examples
{
page.Margin(20);
page.Size(PageSizes.A7.Landscape());
page.Background(Colors.White);
page.PageColor(Colors.White);
page.Header().Text("With ensure space").SemiBold();
+4 -46
View File
@@ -19,11 +19,12 @@ namespace QuestPDF.Examples
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Background(Colors.White);
page.DefaultTextStyle(TextStyle.Default.Size(20));
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hello PDF!").SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
.Text("Hello PDF!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
@@ -48,48 +49,5 @@ namespace QuestPDF.Examples
Process.Start("explorer.exe", "hello.pdf");
}
[Test]
public void MinimalApi2()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Background(Colors.White);
page.DefaultTextStyle(TextStyle.Default.Size(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();
});
});
});
}
}
}
@@ -8,15 +8,15 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class PageBackgroundForeground
public class PageBackgroundForegroundExample
{
[Test]
public void Test()
public void PageBackgroundForeground()
{
RenderingTest
.Create()
.PageSize(550, 400)
.ProducePdf()
.ProduceImages()
.MaxPages(100)
.ShowResults()
.RenderDocument(document =>
{
@@ -25,15 +25,23 @@ namespace QuestPDF.Examples
page.Size(PageSizes.A4);
page.Margin(1, Unit.Inch);
page.DefaultTextStyle(TextStyle.Default.FontSize(16));
page.PageColor(Colors.White);
page.Foreground()
.AlignMiddle()
.AlignCenter()
.Text("Watermark")
.FontSize(64)
.FontColor(Colors.Blue.Lighten3);
const string transparentBlue = "#662196f3";
page.Background()
.AlignTop()
.ExtendHorizontal()
.Height(200)
.Background(transparentBlue);
page.Header().Text("Background and foreground").Bold().FontColor(Colors.Blue.Medium).FontSize(24);
page.Foreground()
.AlignBottom()
.ExtendHorizontal()
.Height(250)
.Background(transparentBlue);
page.Header().Text("Background and foreground").Bold().FontColor(Colors.Blue.Darken2).FontSize(36);
page.Content().PaddingVertical(25).Column(column =>
{
+1 -1
View File
@@ -21,7 +21,7 @@ namespace QuestPDF.Examples
{
page.Margin(20);
page.Size(PageSizes.A7.Landscape());
page.Background(Colors.White);
page.PageColor(Colors.White);
page.Header().Text("With show once").SemiBold();
+1 -1
View File
@@ -21,7 +21,7 @@ namespace QuestPDF.Examples
{
page.Margin(20);
page.Size(PageSizes.A7.Landscape());
page.Background(Colors.White);
page.PageColor(Colors.White);
page.Header().Column(column =>
{
+46
View File
@@ -0,0 +1,46 @@
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
namespace QuestPDF.Examples
{
public class TableBenchmark
{
[Test]
public void Benchmark()
{
RenderingTest
.Create()
.ProducePdf()
.PageSize(PageSizes.A4)
.ShowResults()
.MaxPages(10_000)
.EnableCaching(true)
.EnableDebugging(false)
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Table(table =>
{
const int numberOfRows = 100_000;
const int numberOfColumns = 10;
table.ColumnsDefinition(columns =>
{
foreach (var _ in Enumerable.Range(0, numberOfColumns))
columns.RelativeColumn();
});
foreach (var row in Enumerable.Range(0, numberOfRows))
foreach (var column in Enumerable.Range(0, numberOfColumns))
table.Cell().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}");
});
});
}
}
}
+19 -26
View File
@@ -23,7 +23,7 @@ namespace QuestPDF.Examples
.PageSize(PageSizes.A4)
.ProducePdf()
.ShowResults()
.Render(x => ComposeBook(x, chapters));
.RenderDocument(x => ComposeBook(x, chapters));
}
[Test]
@@ -42,8 +42,8 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProducePdf()
.Render(x => ComposeBook(x, chapters));
.ProducePdf()
.RenderDocument(x => ComposeBook(x, chapters));
}
IEnumerable<float> PerformTest(int attempts)
@@ -102,37 +102,30 @@ namespace QuestPDF.Examples
}
}
private void ComposeBook(IContainer container, ICollection<BookChapter> chapters)
private void ComposeBook(IDocumentContainer container, ICollection<BookChapter> chapters)
{
var subtitleStyle = TextStyle.Default.Size(24).SemiBold().Color(Colors.Blue.Medium);
var normalStyle = TextStyle.Default.Size(14);
ComposePage(container);
void ComposePage(IContainer container)
container.Page(page =>
{
container
.Padding(50)
.Decoration(decoration =>
{
decoration
.Content()
.Column(column =>
{
column.Item().Element(Title);
column.Item().PageBreak();
column.Item().Element(TableOfContents);
column.Item().PageBreak();
page.Margin(50);
page.Content().Column(column =>
{
column.Item().Element(Title);
column.Item().PageBreak();
column.Item().Element(TableOfContents);
column.Item().PageBreak();
Chapters(column);
Chapters(column);
column.Item().Element(Acknowledgements);
});
column.Item().Element(Acknowledgements);
});
page.Footer().Element(Footer);
});
decoration.After().Element(Footer);
});
}
void Title(IContainer container)
{
container
+234 -8
View File
@@ -29,13 +29,106 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.Size(20));
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
text.Span("This is a normal text, followed by an ");
text.Span("underlined red text.").FontSize(20).FontColor(Colors.Red.Medium).Underline();
text.Span("underlined red text").FontColor(Colors.Red.Medium).Underline();
text.Span(".");
});
});
}
[Test]
public void SuperscriptSubscript_General()
{
RenderingTest
.Create()
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(5)
.MinimalBox()
.Border(1)
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(20));
text.ParagraphSpacing(2);
text.Span("In physics, massenergy equivalence is the relationship between mass and energy in a system's rest frame, where the two values differ only by a constant and the units of measurement.");
text.Span("[1][2]").Superscript();
text.Span(" The principle is described by the physicist Albert Einstein's famous formula: E = mc");
text.Span("2").Superscript();
text.Span(". ");
text.Span("[3]").Superscript();
text.EmptyLine();
text.Span("H");
text.Span("2").Subscript();
text.Span("O is the chemical formula for water, meaning that each of its molecules contains one oxygen and two hydrogen atoms.");
text.EmptyLine();
text.Span("H");
text.Span("2").Subscript();
text.Span("O");
});
});
}
[Test]
public void SuperscriptSubscript_Effects()
{
RenderingTest
.Create()
.PageSize(800, 400)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.DefaultTextStyle(x => x.FontSize(30))
.Column(column =>
{
column.Spacing(25);
column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.Underline());
text.Span("Underline of the superscript (E = mc");
text.Span("2").Superscript();
text.Span(") should be at the same height as for normal text.");
});
column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.Underline());
text.Span("Underline of the subscript(H");
text.Span("2").Subscript();
text.Span("O) should be slightly lower than a normal text.");
});
column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.Strikethrough());
text.Span("Strikethrough of both superscript (E=mc");
text.Span("2").Superscript();
text.Span(") and subscript(H");
text.Span("2").Subscript();
text.Span("O) should be visible in the middle of the text.");
});
});
});
}
[Test]
public void ParagraphSpacing()
{
@@ -81,7 +174,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.Size(20));
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
text.Span("This is a random image aligned to the baseline: ");
text.Element()
@@ -191,7 +284,7 @@ namespace QuestPDF.Examples
text.EmptyLine();
text.Span("The new text element also supports injecting custom content between words: ");
text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image);
text.Element().PaddingBottom(-4).Height(16).Width(32).Image(Placeholders.Image);
text.Span(".");
text.EmptyLine();
@@ -203,7 +296,7 @@ namespace QuestPDF.Examples
text.EmptyLine();
text.ExternalLocation("Please visit QuestPDF website", "https://www.questpdf.com");
text.Hyperlink("Please visit QuestPDF website", "https://www.questpdf.com");
text.EmptyLine();
@@ -238,7 +331,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default);
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
text.AlignLeft();
text.ParagraphSpacing(10);
@@ -248,7 +341,7 @@ namespace QuestPDF.Examples
text.Span("and this is slightly bigger text.").FontSize(16);
text.Span("The new text element also supports injecting custom content between words: ");
text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image);
text.Element().PaddingBottom(-4).Height(16).Width(32).Image(Placeholders.Image);
text.Span(".");
text.EmptyLine();
@@ -257,7 +350,7 @@ namespace QuestPDF.Examples
{
text.Line($"{i}: {Placeholders.Paragraph()}");
text.ExternalLocation("Please visit QuestPDF website", "https://www.questpdf.com");
text.Hyperlink("Please visit QuestPDF website", "https://www.questpdf.com");
text.Span("This is page number ");
text.CurrentPageNumber();
@@ -269,5 +362,138 @@ namespace QuestPDF.Examples
});
});
}
[Test]
public void MeasureIssueWhenSpaceAtLineEnd()
{
// issue 135
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);
page.Size(PageSizes.A4);
page.Content().Text("This is a specially crafted sentence with a specially chosen length for demonstration of the bug that occurs ;;;;;. ").FontSize(11).BackgroundColor(Colors.Red.Lighten3);
});
});
}
[Test]
public void EmptyText()
{
// issue 135
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);
page.Size(PageSizes.A4);
page.Content().Text(" ").FontSize(11).BackgroundColor(Colors.Red.Lighten3);
});
});
}
[Test]
public void Whitespaces()
{
// issue 135
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);
page.Size(PageSizes.A4);
page.Content().Text(" x ").FontSize(11).BackgroundColor(Colors.Red.Lighten3);
});
});
}
[Test]
public void DrawingNullTextShouldNotThrowException()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);
page.Size(PageSizes.A4);
page.Content().Column(column =>
{
column.Item().Text(null);
column.Item().Text(text =>
{
text.Span(null);
text.Line(null);
text.Hyperlink(null, "http://www.questpdf.com");
text.TotalPages().Format(x => null);
});
});
});
});
}
[Test]
public void BreakingLongWord()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);
page.Size(PageSizes.A4);
page.Content().Column(column =>
{
column.Item().Text(null);
column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24));
text.Span(" " + Placeholders.LoremIpsum());
text.Span(" 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere();
});
});
});
});
}
}
}
+61
View File
@@ -0,0 +1,61 @@
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
using QuestPDF.ReportSample.Layouts;
//ImagePlaceholder.Solid = true;
// var model = DataSource.GetReport();
// var report = new StandardReport(model);
// report.ShowInPreviewer().Wait();
//
// 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,77 @@
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
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());
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();
}
}
+20
View File
@@ -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();
}
}
}
+202
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Styles>
<FluentTheme Mode="Dark" />
</Application.Styles>
</Application>
+27
View File
@@ -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();
}
}
}
+6
View File
@@ -0,0 +1,6 @@
using SkiaSharp;
namespace QuestPDF.Previewer
{
record PreviewPage(SKPicture Picture, float Width, float Height);
}
+118
View File
@@ -0,0 +1,118 @@
using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
namespace QuestPDF.Previewer
{
class PreviewerControl : Control
{
private InteractiveCanvas InteractiveCanvas { get; set; } = new ();
public static readonly StyledProperty<ObservableCollection<PreviewPage>> PagesProperty =
AvaloniaProperty.Register<PreviewerControl, ObservableCollection<PreviewPage>>(nameof(Pages));
public ObservableCollection<PreviewPage>? Pages
{
get => GetValue(PagesProperty);
set => SetValue(PagesProperty, value);
}
public static readonly StyledProperty<float> CurrentScrollProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(CurrentScroll));
public float CurrentScroll
{
get => GetValue(CurrentScrollProperty);
set => SetValue(CurrentScrollProperty, value);
}
public static readonly StyledProperty<float> ScrollViewportSizeProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(ScrollViewportSize));
public float ScrollViewportSize
{
get => GetValue(ScrollViewportSizeProperty);
set => SetValue(ScrollViewportSizeProperty, value);
}
public PreviewerControl()
{
PagesProperty.Changed.Subscribe(x =>
{
InteractiveCanvas.Pages = x.NewValue.Value;
InvalidateVisual();
});
CurrentScrollProperty.Changed.Subscribe(x =>
{
InteractiveCanvas.ScrollPercentY = x.NewValue.Value;
InvalidateVisual();
});
ClipToBounds = true;
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
if ((e.KeyModifiers & KeyModifiers.Control) != 0)
{
var scaleFactor = 1 + e.Delta.Y / 10f;
var point = new Point(Bounds.Center.X, Bounds.Top) - e.GetPosition(this);
InteractiveCanvas.ZoomToPoint((float)point.X, -(float)point.Y, (float)scaleFactor);
}
if (e.KeyModifiers == KeyModifiers.None)
{
var translation = (float)e.Delta.Y * 25;
InteractiveCanvas.TranslateWithCurrentScale(0, -translation);
}
InvalidateVisual();
}
private bool IsMousePressed { get; set; }
private Vector MousePosition { get; set; }
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (IsMousePressed)
{
var currentPosition = e.GetPosition(this);
var translation = currentPosition - MousePosition;
InteractiveCanvas.TranslateWithCurrentScale((float)translation.X, -(float)translation.Y);
InvalidateVisual();
}
MousePosition = e.GetPosition(this);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
IsMousePressed = true;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
IsMousePressed = false;
}
public override void Render(DrawingContext context)
{
CurrentScroll = InteractiveCanvas.ScrollPercentY;
ScrollViewportSize = InteractiveCanvas.ScrollViewportSizeY;
InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
context.Custom(InteractiveCanvas);
base.Render(context);
}
}
}
@@ -0,0 +1,18 @@
using SkiaSharp;
namespace QuestPDF.Previewer;
internal class DocumentSnapshot
{
public ICollection<PageSnapshot> Pages { get; set; }
public class PageSnapshot
{
public string Id { get; set; }
public float Width { get; set; }
public float Height { get; set; }
public SKPicture Picture { get; set; }
}
}
+97
View File
@@ -0,0 +1,97 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="QuestPDF.Previewer.PreviewerWindow"
x:DataType="previewer:PreviewerWindowViewModel"
x:CompileBindings="True"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="true"
ExtendClientAreaTitleBarHeightHint="-1"
Background="#666"
Icon="/Resources/Logo.png"
UseLayoutRounding="True"
Title="QuestPDF Document Preview">
<Window.Styles>
<Style Selector="Button.actions">
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Padding" Value="10"/>
<Setter Property="CornerRadius" Value="100"/>
<Setter Property="Background" Value="#888"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#999"/>
</Style>
</Window.Styles>
<Panel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center" HorizontalAlignment="Center"
TextAlignment="Center" Text="QuestPDF Previewer" FontSize="14" Foreground="#DFFF" FontWeight="Regular" IsHitTestVisible="False" />
<previewer:PreviewerControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
Pages="{Binding Pages, Mode=OneWay}" />
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
<Button Classes="actions"
Command="{Binding ShowPdfCommand, Mode=OneTime}"
IsVisible="{Binding !!Pages.Count}"
ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H13C12.59,21.75 12.2,21.44 11.86,21.1C11.53,20.77 11.25,20.4 11,20H6V4H13V9H18V10.18C18.71,10.34 19.39,10.61 20,11V8L14,2M20.31,18.9C21.64,16.79 21,14 18.91,12.68C16.8,11.35 14,12 12.69,14.08C11.35,16.19 12,18.97 14.09,20.3C15.55,21.23 17.41,21.23 18.88,20.32L22,23.39L23.39,22L20.31,18.9M16.5,19A2.5,2.5 0 0,1 14,16.5A2.5,2.5 0 0,1 16.5,14A2.5,2.5 0 0,1 19,16.5A2.5,2.5 0 0,1 16.5,19Z" />
</Canvas>
</Viewbox>
</Button>
<Button Classes="actions"
Command="{Binding ShowDocumentationCommand, Mode=OneTime}"
ToolTip.Tip="Opens official QuestPDF documentation">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z" />
</Canvas>
</Viewbox>
</Button>
<Button Classes="actions"
Command="{Binding SponsorProjectCommand, Mode=OneTime}"
ToolTip.Tip="Do you like QuestPDF? Please consider sponsoring the project. It really helps!">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M12,21.1L10.5,22.4C3.9,16.5 0.5,13.4 0.5,9.6C0.5,8.4 0.9,7.3 1.5,6.4C1.5,6.6 1.5,6.8 1.5,7C1.5,11.7 5.4,15.2 12,21.1M13.6,17C18.3,12.7 21.5,9.9 21.6,7C21.6,5 20.1,3.5 18.1,3.5C16.5,3.5 15,4.5 14.5,5.9H12.6C12,4.5 10.5,3.5 9,3.5C7,3.5 5.5,5 5.5,7C5.5,9.9 8.6,12.7 13.4,17L13.5,17.1M18,1.5C21.1,1.5 23.5,3.9 23.5,7C23.5,10.7 20.1,13.8 13.5,19.8C6.9,13.9 3.5,10.8 3.5,7C3.5,3.9 5.9,1.5 9,1.5C10.7,1.5 12.4,2.3 13.5,3.6C14.6,2.3 16.3,1.5 18,1.5Z" />
</Canvas>
</Viewbox>
</Button>
</StackPanel>
<ScrollBar Grid.Row="1" Grid.Column="1"
Orientation="Vertical"
AllowAutoHide="False"
Minimum="0" Maximum="1"
IsVisible="{Binding VerticalScrollbarVisible, Mode=OneWay}"
Value="{Binding CurrentScroll, Mode=TwoWay}"
ViewportSize="{Binding ScrollViewportSize, Mode=OneWay}"></ScrollBar>
</Grid>
</Panel>
</Window>
@@ -0,0 +1,23 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace QuestPDF.Previewer
{
class PreviewerWindow : Window
{
public PreviewerWindow()
{
InitializeComponent();
}
protected override void OnClosed(EventArgs e)
{
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,89 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using ReactiveUI;
using Unit = System.Reactive.Unit;
using Avalonia.Threading;
namespace QuestPDF.Previewer
{
internal class PreviewerWindowViewModel : ReactiveObject
{
private ObservableCollection<PreviewPage> _pages = new();
public ObservableCollection<PreviewPage> Pages
{
get => _pages;
set => this.RaiseAndSetIfChanged(ref _pages, value);
}
private float _currentScroll;
public float CurrentScroll
{
get => _currentScroll;
set => this.RaiseAndSetIfChanged(ref _currentScroll, value);
}
private float _scrollViewportSize;
public float ScrollViewportSize
{
get => _scrollViewportSize;
set
{
this.RaiseAndSetIfChanged(ref _scrollViewportSize, value);
VerticalScrollbarVisible = value < 1;
}
}
private bool _verticalScrollbarVisible;
public bool VerticalScrollbarVisible
{
get => _verticalScrollbarVisible;
private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _verticalScrollbarVisible, value));
}
public ReactiveCommand<Unit, Unit> ShowPdfCommand { get; }
public ReactiveCommand<Unit, Unit> ShowDocumentationCommand { get; }
public ReactiveCommand<Unit, Unit> SponsorProjectCommand { get; }
public PreviewerWindowViewModel()
{
CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html"));
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
}
private void ShowPdf()
{
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf");
Helpers.GeneratePdfFromDocumentSnapshots(filePath, Pages);
OpenLink(filePath);
}
private void OpenLink(string path)
{
using var openBrowserProcess = new Process
{
StartInfo = new()
{
UseShellExecute = true,
FileName = path
}
};
openBrowserProcess.Start();
}
private void HandleUpdatePreview(ICollection<PreviewPage> pages)
{
var oldPages = Pages;
Pages.Clear();
Pages = new ObservableCollection<PreviewPage>(pages);
foreach (var page in oldPages)
page.Picture.Dispose();
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI;
using QuestPDF.Previewer;
var applicationPort = GetCommunicationPort();
CommunicationService.Instance.Start(applicationPort);
if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new PreviewerWindow()
{
DataContext = new PreviewerWindowViewModel()
};
desktop.MainWindow.Show();
desktop.Start(Array.Empty<string>());
return;
}
AppBuilder
.Configure(() => new PreviewerApp())
.UsePlatformDetect()
.UseReactiveUI()
.StartWithClassicDesktopLifetime(Array.Empty<string>());
static int GetCommunicationPort()
{
const int defaultApplicationPort = 12500;
var arguments = Environment.GetCommandLineArgs();
if (arguments.Length < 2)
return defaultApplicationPort;
return int.TryParse(arguments[1], out var port) ? port : defaultApplicationPort;
}
@@ -0,0 +1,54 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.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>
<PackageReleaseNotes>Initial release.</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 previewer</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<OutputType>exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Resources\Logo.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Logo.png" />
<None Include="Resources\Logo.png">
<Pack>true</Pack>
<Visible>false</Visible>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Avalonia" Version="0.10.10" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
</ItemGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+23
View File
@@ -0,0 +1,23 @@
Install nuget locally (in directory where nupkg file is located)
```
dotnet tool install --global --add-source . QuestPDF.Previewer --global
```
Run on default port
```
questpdf-previewer
```
Run on custom port
```
questpdf-previewer 12500
```
Remove nuget locally
```
dotnet tool uninstall QuestPDF.Previewer --global
```
@@ -1,3 +1,4 @@
using System;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
@@ -47,7 +48,7 @@ namespace QuestPDF.ReportSample.Layouts
container.Border(0.75f).BorderColor(Colors.Grey.Medium).Grid(grid =>
{
grid.Columns(6);
grid.Item().LabelCell().Text("Date");
grid.Item(2).ValueCell().Text(Model.Date?.ToString("g") ?? string.Empty);
grid.Item().LabelCell().Text("Location");
+17
View File
@@ -8,6 +8,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.UnitTests", "Quest
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer.Examples", "QuestPDF.Previewer.Examples\QuestPDF.Previewer.Examples.csproj", "{CA413A39-038F-4A9F-B56F-0E5413B6B158}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{73123649-216A-47B4-BFB2-DADF13FBE75D}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{B2FF6003-3A45-4A78-A85D-B86C7F01D054}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -30,5 +39,13 @@ Global
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.Build.0 = Release|Any CPU
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
-6
View File
@@ -1,6 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("QuestPDF.UnitTests")]
[assembly: InternalsVisibleTo("QuestPDF.Examples")]
[assembly: InternalsVisibleTo("QuestPDF.ReportSample")]
+27 -6
View File
@@ -17,19 +17,30 @@ namespace QuestPDF.Drawing
{
internal static void GeneratePdf(Stream stream, IDocument document)
{
CheckIfStreamIsCompatible(stream);
var metadata = document.GetMetadata();
var writeOnlyStream = new WriteOnlyStream(stream);
var canvas = new PdfCanvas(writeOnlyStream, metadata);
var canvas = new PdfCanvas(stream, metadata);
RenderDocument(canvas, document);
}
internal static void GenerateXps(Stream stream, IDocument document)
{
CheckIfStreamIsCompatible(stream);
var metadata = document.GetMetadata();
var writeOnlyStream = new WriteOnlyStream(stream);
var canvas = new XpsCanvas(writeOnlyStream, metadata);
var canvas = new XpsCanvas(stream, metadata);
RenderDocument(canvas, document);
}
private static void CheckIfStreamIsCompatible(Stream stream)
{
if (!stream.CanWrite)
throw new ArgumentException("The library requires a Stream object with the 'write' capability available (the CanWrite flag). Please consider using the MemoryStream class.");
if (!stream.CanSeek)
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)
{
@@ -40,7 +51,14 @@ namespace QuestPDF.Drawing
return canvas.Images;
}
private static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
{
var canvas = new SkiaPictureCanvas();
RenderDocument(canvas, document);
return canvas.Pictures;
}
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
where TCanvas : ICanvas, IRenderingCanvas
{
var container = new DocumentContainer();
@@ -146,7 +164,7 @@ namespace QuestPDF.Drawing
return debuggingState;
}
private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
{
if (content == null)
return;
@@ -168,6 +186,9 @@ namespace QuestPDF.Drawing
return;
}
if (content is DynamicHost dynamicHost)
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
var targetTextStyle = documentDefaultTextStyle;
if (content is DefaultTextStyle defaultTextStyleElement)
+27 -3
View File
@@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using SkiaSharp;
@@ -52,7 +53,8 @@ namespace QuestPDF.Drawing
{
return new SKPaint
{
Color = SKColor.Parse(color)
Color = SKColor.Parse(color),
IsAntialias = true
};
}
}
@@ -67,13 +69,24 @@ namespace QuestPDF.Drawing
{
Color = SKColor.Parse(style.Color),
Typeface = GetTypeface(style),
TextSize = style.Size ?? 12
TextSize = (style.Size ?? 12) * GetTextScale(style),
IsAntialias = true,
};
}
static SKTypeface GetTypeface(TextStyle style)
{
var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
// superscript and subscript use slightly bolder font to match visually line thickness
if (style.FontPosition is FontPosition.Superscript or FontPosition.Subscript)
{
var weightValue = (int)weight;
weightValue = Math.Min(weightValue + 100, 1000);
weight = (SKFontStyleWeight) (weightValue);
}
var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
@@ -92,11 +105,22 @@ namespace QuestPDF.Drawing
$"1) install the font on your operating system or execution environment. " +
$"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method.");
}
static float GetTextScale(TextStyle style)
{
return style.FontPosition switch
{
FontPosition.Normal => 1f,
FontPosition.Subscript => 0.625f,
FontPosition.Superscript => 0.625f,
_ => throw new ArgumentOutOfRangeException()
};
}
}
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
{
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
}
}
}
+4 -4
View File
@@ -6,16 +6,16 @@ namespace QuestPDF.Drawing
{
internal class SkiaDocumentCanvasBase : SkiaCanvasBase
{
private SKDocument Document { get; }
private SKDocument? Document { get; }
public SkiaDocumentCanvasBase(SKDocument document)
protected SkiaDocumentCanvasBase(SKDocument document)
{
Document = document;
}
~SkiaDocumentCanvasBase()
{
Document.Dispose();
Document?.Dispose();
}
public override void BeginDocument()
@@ -25,7 +25,7 @@ namespace QuestPDF.Drawing
public override void EndDocument()
{
Canvas.Dispose();
Canvas?.Dispose();
Document.Close();
Document.Dispose();
+55
View File
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Drawing
{
public class PreviewerPicture
{
public SKPicture Picture { get; set; }
public Size Size { get; set; }
public PreviewerPicture(SKPicture picture, Size size)
{
Picture = picture;
Size = size;
}
}
internal class SkiaPictureCanvas : SkiaCanvasBase
{
private SKPictureRecorder? PictureRecorder { get; set; }
private Size? CurrentPageSize { get; set; }
public ICollection<PreviewerPicture> Pictures { get; } = new List<PreviewerPicture>();
public override void BeginDocument()
{
Pictures.Clear();
}
public override void BeginPage(Size size)
{
CurrentPageSize = size;
PictureRecorder = new SKPictureRecorder();
Canvas = PictureRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
}
public override void EndPage()
{
var picture = PictureRecorder?.EndRecording();
if (picture != null && CurrentPageSize.HasValue)
Pictures.Add(new PreviewerPicture(picture, CurrentPageSize.Value));
PictureRecorder?.Dispose();
PictureRecorder = null;
}
public override void EndDocument() { }
}
}
+104
View File
@@ -0,0 +1,104 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class DynamicHost : Element, IStateResettable
{
private DynamicComponentProxy Child { get; }
private object InitialComponentState { get; set; }
internal TextStyle TextStyle { get; } = new();
public DynamicHost(DynamicComponentProxy child)
{
Child = child;
InitialComponentState = Child.GetState();
}
public void ResetState()
{
Child.SetState(InitialComponentState);
}
internal override SpacePlan Measure(Size availableSpace)
{
var result = GetContent(availableSpace, acceptNewState: false);
var content = result.Content as Element ?? Empty.Instance;
var measurement = content.Measure(availableSpace);
if (measurement.Type != SpacePlanType.FullRender)
throw new DocumentLayoutException("Dynamic component generated content that does not fit on a single page.");
return result.HasMoreContent
? SpacePlan.PartialRender(measurement)
: SpacePlan.FullRender(measurement);
}
internal override void Draw(Size availableSpace)
{
var content = GetContent(availableSpace, acceptNewState: true).Content as Element;
content?.Draw(availableSpace);
}
private DynamicComponentComposeResult GetContent(Size availableSize, bool acceptNewState)
{
var componentState = Child.GetState();
var context = new DynamicContext
{
PageNumber = PageContext.CurrentPage,
PageContext = PageContext,
Canvas = Canvas,
TextStyle = TextStyle,
AvailableSize = availableSize
};
var result = Child.Compose(context);
if (!acceptNewState)
Child.SetState(componentState);
return result;
}
}
public class DynamicContext
{
internal IPageContext PageContext { get; set; }
internal ICanvas Canvas { get; set; }
internal TextStyle TextStyle { get; set; }
public int PageNumber { get; internal set; }
public Size AvailableSize { get; internal set; }
public IDynamicElement CreateElement(Action<IContainer> content)
{
var container = new DynamicElement();
content(container);
container.ApplyDefaultTextStyle(TextStyle);
container.VisitChildren(x => x?.Initialize(PageContext, Canvas));
container.VisitChildren(x => (x as IStateResettable)?.ResetState());
container.Size = container.Measure(Size.Max);
return container;
}
}
public interface IDynamicElement : IElement
{
Size Size { get; }
}
internal class DynamicElement : ContainerElement, IDynamicElement
{
public Size Size { get; internal set; }
}
}
+2 -2
View File
@@ -25,8 +25,8 @@ namespace QuestPDF.Elements
{
return Type switch
{
LineType.Vertical when availableSpace.Width >= Size => SpacePlan.FullRender(Size, 0),
LineType.Horizontal when availableSpace.Height >= Size => SpacePlan.FullRender(0, Size),
LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(Size, 0),
LineType.Horizontal when availableSpace.Height + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(0, Size),
_ => SpacePlan.Wrap()
};
}
+2 -3
View File
@@ -30,6 +30,7 @@ namespace QuestPDF.Elements
public void Compose(IContainer container)
{
container
.Background(BackgroundColor)
.Layers(layers =>
{
layers
@@ -44,9 +45,7 @@ namespace QuestPDF.Elements
.MaxWidth(MaxSize.Width)
.MaxHeight(MaxSize.Height)
.Background(BackgroundColor)
.PaddingLeft(MarginLeft)
.PaddingRight(MarginRight)
.PaddingTop(MarginTop)
+4 -2
View File
@@ -17,8 +17,10 @@ namespace QuestPDF.Elements
if (perfectScale == null)
return SpacePlan.Wrap();
var targetSpace = ScaleSize(availableSpace, perfectScale.Value);
return SpacePlan.FullRender(targetSpace);
var scaledSpace = ScaleSize(availableSpace, 1 / perfectScale.Value);
var childSizeInScale = Child.Measure(scaledSpace);
var childSizeInOriginalScale = ScaleSize(childSizeInScale, perfectScale.Value);
return SpacePlan.FullRender(childSizeInOriginalScale);
}
internal override void Draw(Size availableSpace)
+37 -2
View File
@@ -19,15 +19,22 @@ namespace QuestPDF.Elements.Table
private int RowsCount { get; set; }
private int CurrentRow { get; set; }
// cache that stores all cells
// first index: row number
// inner table: list of all cells that ends at the corresponding row
private TableCell[][] CellsCache { get; set; }
private int MaxRow { get; set; }
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
{
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
BuildCache();
base.Initialize(pageContext, canvas);
}
internal override IEnumerable<Element?> GetChildren()
{
return Cells;
@@ -38,6 +45,31 @@ namespace QuestPDF.Elements.Table
Cells.ForEach(x => x.IsRendered = false);
CurrentRow = 1;
}
private void BuildCache()
{
if (CellsCache != null)
return;
if (Cells.Count == 0)
{
MaxRow = 0;
CellsCache = Array.Empty<TableCell[]>();
return;
}
var groups = Cells
.GroupBy(x => x.Row + x.RowSpan - 1)
.ToDictionary(x => x.Key, x => x.OrderBy(x => x.Column).ToArray());
MaxRow = groups.Max(x => x.Key);
CellsCache = Enumerable
.Range(0, MaxRow + 1)
.Select(x => groups.ContainsKey(x) ? groups[x] : Array.Empty<TableCell>())
.ToArray();
}
internal override SpacePlan Measure(Size availableSpace)
{
@@ -143,7 +175,10 @@ namespace QuestPDF.Elements.Table
var rowBottomOffsets = new DynamicDictionary<int, float>();
var commands = new List<TableCellRenderingCommand>();
var cellsToTry = Cells.Where(x => x.Row + x.RowSpan - 1 >= CurrentRow);
var cellsToTry = Enumerable
.Range(CurrentRow, MaxRow - CurrentRow + 1)
.SelectMany(x => CellsCache[x]);
var currentRow = CurrentRow;
var maxRenderingRow = RowsCount;
@@ -9,6 +9,8 @@ namespace QuestPDF.Elements.Text.Calculation
public int StartIndex { get; set; }
public float AvailableWidth { get; set; }
public bool IsFirstLineElement { get; set; }
public bool IsFirstElementInBlock { get; set; }
public bool IsFirstElementInLine { get; set; }
}
}
@@ -11,19 +11,19 @@ namespace QuestPDF.Elements.Text.Items
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
SetPageNumber(request.PageContext);
UpdatePageNumberText(request.PageContext);
return MeasureWithoutCache(request);
}
public override void Draw(TextDrawingRequest request)
{
SetPageNumber(request.PageContext);
UpdatePageNumberText(request.PageContext);
base.Draw(request);
}
private void SetPageNumber(IPageContext context)
private void UpdatePageNumberText(IPageContext context)
{
Text = Source(context) ?? PageNumberPlaceholder;
Text = Source(context) ?? string.Empty;
}
}
}
+82 -41
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
@@ -8,16 +9,17 @@ namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSpan : ITextBlockItem
{
private const char Space = ' ';
public string Text { get; set; }
public TextStyle Style { get; set; } = new TextStyle();
public TextStyle Style { get; set; } = new();
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache =
new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>();
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
{
var cacheKey = (request.StartIndex, request.AvailableWidth);
if (!MeasureCache.ContainsKey(cacheKey))
MeasureCache[cacheKey] = MeasureWithoutCache(request);
@@ -26,20 +28,20 @@ namespace QuestPDF.Elements.Text.Items
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
{
const char space = ' ';
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
var startIndex = request.StartIndex;
if (request.IsFirstLineElement)
// if the element is the first one within the line,
// ignore leading spaces
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
{
while (startIndex + 1 < Text.Length && Text[startIndex] == space)
while (startIndex < Text.Length && Text[startIndex] == Space)
startIndex++;
}
if (Text.Length == 0)
if (Text.Length == 0 || startIndex == Text.Length)
{
return new TextMeasurementResult
{
@@ -52,40 +54,25 @@ namespace QuestPDF.Elements.Text.Items
}
// start breaking text from requested position
var text = Text.Substring(startIndex);
var text = Text.AsSpan().Slice(startIndex);
var textLength = (int)paint.BreakText(text, request.AvailableWidth);
var textLength = (int)paint.BreakText(text, request.AvailableWidth + Size.Epsilon);
if (textLength <= 0)
return null;
if (textLength < text.Length && text[textLength] == space)
textLength++;
// break text only on spaces
if (textLength < text.Length)
{
var lastSpaceIndex = text.Substring(0, textLength).LastIndexOf(space) - 1;
var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine);
if (lastSpaceIndex <= 0)
{
if (!request.IsFirstLineElement)
return null;
}
else
{
textLength = lastSpaceIndex + 1;
}
}
if (wrappedTextLength == null)
return null;
text = text.Substring(0, textLength);
textLength = wrappedTextLength.Value.fragmentLength;
text = text.Slice(0, textLength);
var endIndex = startIndex + textLength;
var nextIndex = endIndex;
while (nextIndex + 1 < Text.Length && Text[nextIndex] == space)
nextIndex++;
// measure final text
var width = paint.MeasureText(text);
@@ -100,31 +87,85 @@ namespace QuestPDF.Elements.Text.Items
StartIndex = startIndex,
EndIndex = endIndex,
NextIndex = nextIndex,
NextIndex = startIndex + wrappedTextLength.Value.nextIndex,
TotalIndex = Text.Length
};
}
// TODO: consider introduce text wrapping abstraction (basic, english-like, asian-like)
private (int fragmentLength, int nextIndex)? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
{
// textLength - length of the part of the text that fits in available width (creating a line)
// entire text fits, no need to wrap
if (textLength == text.Length)
return (textLength, textLength + 1);
// breaking anywhere
if (Style.WrapAnywhere ?? false)
return (textLength, textLength);
// current line ends at word, next character is space, perfect place to wrap
if (text[textLength - 1] != Space && text[textLength] == Space)
return (textLength, textLength + 1);
// find last space within the available text to wrap
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(Space);
// text contains space that can be used to wrap
if (lastSpaceIndex > 0)
return (lastSpaceIndex, lastSpaceIndex + 1);
// there is no available space to wrap text
// if the item is first within the line, perform safe mode and chop the word
// otherwise, move the item into the next line
return isFirstElementInLine ? (textLength, textLength) : null;
}
public virtual void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
var glyphOffset = GetGlyphOffset();
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
request.Canvas.DrawText(text, Position.Zero, Style);
request.Canvas.DrawText(text, new Position(0, glyphOffset), Style);
// draw underline
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1);
{
var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffset;
DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1);
}
// draw stroke
if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue)
DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness ?? 1);
{
var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1;
strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f;
DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffset, strikeoutThickness);
}
void DrawLine(float offset, float thickness)
{
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
request.Canvas.DrawRectangle(new Position(0, offset), new Size(request.TextSize.Width, thickness), Style.Color);
}
float GetGlyphOffset()
{
var fontSize = Style.Size ?? 12f;
var offsetFactor = Style.FontPosition switch
{
FontPosition.Normal => 0,
FontPosition.Subscript => 0.1f,
FontPosition.Superscript => -0.35f,
_ => throw new ArgumentOutOfRangeException()
};
return fontSize * offsetFactor;
}
}
}
+4 -2
View File
@@ -108,7 +108,7 @@ namespace QuestPDF.Elements.Text
.ForEach(x => RenderingQueue.Dequeue());
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.EndIndex;
if (!RenderingQueue.Any())
ResetState();
@@ -170,7 +170,9 @@ namespace QuestPDF.Elements.Text
StartIndex = currentItemIndex,
AvailableWidth = availableWidth - currentWidth,
IsFirstLineElement = !currentLineElements.Any()
IsFirstElementInBlock = currentElement == Items.First(),
IsFirstElementInLine = !currentLineElements.Any()
};
var measurementResponse = currentElement.Measure(measurementRequest);
@@ -0,0 +1,14 @@
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class DynamicComponentExtensions
{
public static void Dynamic<TState>(this IContainer element, IDynamicComponent<TState> dynamicElement) where TState : struct
{
var componentProxy = DynamicComponentProxy.CreateFrom(dynamicElement);
element.Element(new DynamicHost(componentProxy));
}
}
}
+2 -2
View File
@@ -29,6 +29,8 @@ namespace QuestPDF.Fluent
}
#endregion
#region XPS
public static byte[] GenerateXps(this IDocument document)
{
@@ -48,8 +50,6 @@ namespace QuestPDF.Fluent
DocumentGenerator.GenerateXps(stream, document);
}
#region XPS
#endregion
#region Images
+7 -1
View File
@@ -88,11 +88,17 @@ namespace QuestPDF.Fluent
DefaultTextStyle(handler(TextStyle.Default));
}
public void Background(string color)
public void PageColor(string color)
{
Page.BackgroundColor = color;
}
[Obsolete("This element has been renamed since version 2022.3. Please use the PageColor method.")]
public void Background(string color)
{
PageColor(color);
}
public IContainer Background()
{
var container = new Container();
+9 -5
View File
@@ -24,7 +24,7 @@ namespace QuestPDF.Fluent
public class TextPageNumberDescriptor : TextSpanDescriptor
{
internal PageNumberFormatter FormatFunction { get; private set; } = x => (x ?? 123).ToString();
internal PageNumberFormatter FormatFunction { get; private set; } = x => x?.ToString() ?? string.Empty;
internal TextPageNumberDescriptor(TextStyle textStyle) : base(textStyle)
{
@@ -92,6 +92,10 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor Span(string? text)
{
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
if (text == null)
return descriptor;
var items = text
.Replace("\r", string.Empty)
@@ -114,7 +118,7 @@ namespace QuestPDF.Fluent
.ToList()
.ForEach(TextBlocks.Add);
return new TextSpanDescriptor(style);
return descriptor;
}
public TextSpanDescriptor Line(string? text)
@@ -131,7 +135,7 @@ namespace QuestPDF.Fluent
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
{
var style = DefaultStyle.Clone();
var descriptor = new TextPageNumberDescriptor(DefaultStyle);
var descriptor = new TextPageNumberDescriptor(style);
AddItemToLastTextBlock(new TextBlockPageNumber
{
@@ -181,7 +185,7 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor SectionLink(string? text, string sectionName)
{
if (IsNullOrEmpty(sectionName))
throw new ArgumentException(nameof(sectionName));
throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
@@ -208,7 +212,7 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor Hyperlink(string? text, string url)
{
if (IsNullOrEmpty(url))
throw new ArgumentException(nameof(url));
throw new ArgumentException("Url cannot be null or empty", nameof(url));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
@@ -63,6 +63,12 @@ namespace QuestPDF.Fluent
return descriptor;
}
public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.WrapAnywhere = value;
return descriptor;
}
#region Weight
public static T Weight<T>(this T descriptor, FontWeight weight) where T : TextSpanDescriptor
@@ -120,7 +126,30 @@ namespace QuestPDF.Fluent
{
return descriptor.Weight(FontWeight.ExtraBlack);
}
#endregion
#region Position
public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Normal);
}
public static T Subscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Subscript);
}
public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Superscript);
}
private static T Position<T>(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontPosition = fontPosition;
return descriptor;
}
#endregion
}
}
+32 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
@@ -70,6 +71,11 @@ namespace QuestPDF.Fluent
{
return style.Mutate(x => x.HasUnderline = value);
}
public static TextStyle WrapAnywhere(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.WrapAnywhere = value);
}
#region Weight
@@ -127,7 +133,32 @@ namespace QuestPDF.Fluent
{
return style.Weight(FontWeight.ExtraBlack);
}
#endregion
#region Position
public static TextStyle NormalPosition(this TextStyle style)
{
return style.Position(FontPosition.Normal);
}
public static TextStyle Subscript(this TextStyle style)
{
return style.Position(FontPosition.Subscript);
}
public static TextStyle Superscript(this TextStyle style)
{
return style.Position(FontPosition.Superscript);
}
private static TextStyle Position(this TextStyle style, FontPosition fontPosition)
{
if (style.FontPosition == fontPosition)
return style;
return style.Mutate(t => t.FontPosition = fontPosition);
}
#endregion
}
}
-56
View File
@@ -1,56 +0,0 @@
using System;
using System.IO;
namespace QuestPDF.Helpers
{
/// <summary>
/// SkiaSharp calls the Position property when generating target document file.
/// If the output stream does not support the Position property, the NullReferenceException is thrown.
/// This wrapper fixes this issue by providing cached Position value (always the end of the stream / current length).
/// Example stream affected: HttpContext.Response.Body
/// </summary>
internal class WriteOnlyStream : Stream
{
private readonly Stream InnerStream;
private long StreamLength { get; set; }
public WriteOnlyStream(Stream stream)
{
if (!stream.CanWrite)
throw new NotSupportedException("Stream cannot be written");
InnerStream = stream;
}
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => StreamLength;
public override long Position
{
get => StreamLength;
set => throw new NotImplementedException();
}
public override void Flush() => InnerStream.Flush();
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotImplementedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotImplementedException();
public override void SetLength(long value)
=> throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count)
{
InnerStream.Write(buffer, offset, count);
StreamLength += count;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace QuestPDF.Infrastructure
{
internal enum FontPosition
{
Normal,
Subscript,
Superscript,
}
}
@@ -0,0 +1,35 @@
using System;
using QuestPDF.Elements;
using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure
{
internal class DynamicComponentProxy
{
internal Action<object> SetState { get; private set; }
internal Func<object> GetState { get; private set; }
internal Func<DynamicContext, DynamicComponentComposeResult> Compose { get; private set; }
internal static DynamicComponentProxy CreateFrom<TState>(IDynamicComponent<TState> component) where TState : struct
{
return new DynamicComponentProxy
{
GetState = () => component.State,
SetState = x => component.State = (TState)x,
Compose = component.Compose
};
}
}
public class DynamicComponentComposeResult
{
public IElement Content { get; set; }
public bool HasMoreContent { get; set; }
}
public interface IDynamicComponent<TState> where TState : struct
{
TState State { get; set; }
DynamicComponentComposeResult Compose(DynamicContext context);
}
}
+10 -2
View File
@@ -13,9 +13,11 @@ namespace QuestPDF.Infrastructure
internal float? Size { get; set; }
internal float? LineHeight { get; set; }
internal FontWeight? FontWeight { get; set; }
internal FontPosition? FontPosition { get; set; }
internal bool? IsItalic { get; set; }
internal bool? HasStrikethrough { get; set; }
internal bool? HasUnderline { get; set; }
internal bool? WrapAnywhere { get; set; }
internal object PaintKey { get; private set; }
internal object FontMetricsKey { get; private set; }
@@ -28,9 +30,11 @@ namespace QuestPDF.Infrastructure
Size = 12,
LineHeight = 1.2f,
FontWeight = Infrastructure.FontWeight.Normal,
FontPosition = Infrastructure.FontPosition.Normal,
IsItalic = false,
HasStrikethrough = false,
HasUnderline = false
HasUnderline = false,
WrapAnywhere = false
};
public static TextStyle Default => new TextStyle();
@@ -43,7 +47,7 @@ namespace QuestPDF.Infrastructure
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
PaintKey ??= (FontFamily, Size, FontWeight, IsItalic, Color);
PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic);
}
@@ -55,9 +59,11 @@ namespace QuestPDF.Infrastructure
Size ??= parentStyle.Size;
LineHeight ??= parentStyle.LineHeight;
FontWeight ??= parentStyle.FontWeight;
FontPosition ??= parentStyle.FontPosition;
IsItalic ??= parentStyle.IsItalic;
HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline;
WrapAnywhere ??= parentStyle.WrapAnywhere;
}
internal void OverrideStyle(TextStyle parentStyle)
@@ -68,9 +74,11 @@ namespace QuestPDF.Infrastructure
Size = parentStyle.Size ?? Size;
LineHeight = parentStyle.LineHeight ?? LineHeight;
FontWeight = parentStyle.FontWeight ?? FontWeight;
FontPosition = parentStyle.FontPosition ?? FontPosition;
IsItalic = parentStyle.IsItalic ?? IsItalic;
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
}
internal TextStyle Clone()
+108
View File
@@ -0,0 +1,108 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Previewer
{
public class ExceptionDocument : IDocument
{
private Exception Exception { get; }
public ExceptionDocument(Exception exception)
{
Exception = exception;
}
public DocumentMetadata GetMetadata()
{
return DocumentMetadata.Default;
}
public void Compose(IDocumentContainer document)
{
document.Page(page =>
{
page.ContinuousSize(PageSizes.A4.Width);
page.Margin(50);
page.DefaultTextStyle(x => x.FontSize(16));
page.Foreground().PaddingTop(5).Border(10).BorderColor(Colors.Red.Medium);
page.Header()
.ShowOnce()
.PaddingBottom(5)
.Row(row =>
{
row.Spacing(15);
row.AutoItem()
.PaddingTop(15)
.Width(48)
.AspectRatio(1)
.Canvas((canvas, size) =>
{
const float iconSize = 24;
using var iconPath = SKPath.ParseSvgPathData("M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7");
using var paint = new SKPaint()
{
Color = SKColors.Red,
IsAntialias = true
};
canvas.Scale(size.Width / iconSize);
canvas.DrawPath(iconPath, paint);
});
row.RelativeItem()
.Column(column =>
{
column.Item().Text("Exception").FontSize(36).FontColor(Colors.Red.Medium).Bold();
column.Item().PaddingTop(-10).Text("Don't panic! Just analyze what's happened...").FontSize(18).FontColor(Colors.Red.Medium).Bold();
});
});
page.Content().PaddingVertical(20).Column(column =>
{
var currentException = Exception;
while (currentException != null)
{
column.Item()
.PaddingTop(25)
.PaddingBottom(15)
.Padding(-10)
.Background(Colors.Grey.Lighten3)
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(16));
text.Span(currentException.GetType().Name + ": ").Bold();
text.Span(currentException.Message);
});
foreach (var trace in currentException.StackTrace.Split('\n'))
{
column
.Item()
.ShowEntire()
.BorderBottom(1)
.BorderColor(Colors.Grey.Lighten2)
.PaddingVertical(5)
.Text(trace.Trim())
.FontSize(12);
}
currentException = currentException.InnerException;
}
});
});
}
}
}
+23
View File
@@ -0,0 +1,23 @@
#if NET6_0_OR_GREATER
using System;
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))]
namespace QuestPDF.Previewer
{
/// <summary>
/// Helper for subscribing to hot reload notifications.
/// </summary>
internal static class HotReloadManager
{
public static event EventHandler? UpdateApplicationRequested;
public static void UpdateApplication(Type[]? _)
{
UpdateApplicationRequested?.Invoke(null, EventArgs.Empty);
}
}
}
#endif
+66
View File
@@ -0,0 +1,66 @@
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
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)
{
var previewerService = new PreviewerService(port);
using var cancellationTokenSource = new CancellationTokenSource();
previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
await previewerService.Connect();
await RefreshPreview();
HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
await WaitForPreviewerExit(cancellationTokenSource.Token);
Task RefreshPreview()
{
var pictures = GetPictures();
return previewerService.RefreshPreview(pictures);
ICollection<PreviewerPicture> GetPictures()
{
try
{
return DocumentGenerator.GeneratePreviewerPictures(document);
}
catch (Exception exception)
{
var exceptionDocument = new ExceptionDocument(exception);
return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument);
}
}
}
async Task WaitForPreviewerExit(CancellationToken cancellationToken)
{
while (true)
{
if (cancellationToken.IsCancellationRequested)
return;
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
}
}
}
#endif
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
#if NET6_0_OR_GREATER
namespace QuestPDF.Previewer
{
internal class PreviewerRefreshCommand
{
public ICollection<Page> Pages { get; set; }
public class Page
{
public string Id { get; } = Guid.NewGuid().ToString("N");
public float Width { get; init; }
public float Height { get; init; }
}
}
}
#endif
+161
View File
@@ -0,0 +1,161 @@
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
namespace QuestPDF.Previewer
{
internal class PreviewerService
{
private int Port { get; }
private HttpClient HttpClient { get; }
public event Action? OnPreviewerStopped;
public PreviewerService(int port)
{
Port = port;
HttpClient = new()
{
BaseAddress = new Uri($"http://localhost:{port}/"),
Timeout = TimeSpan.FromSeconds(1)
};
}
public async Task Connect()
{
var isAvailable = await IsPreviewerAvailable();
if (!isAvailable)
{
StartPreviewer();
await WaitForConnection();
}
var previewerVersion = await GetPreviewerVersion();
CheckVersionCompatibility(previewerVersion);
}
private async Task<bool> IsPreviewerAvailable()
{
try
{
using var result = await HttpClient.GetAsync("/ping");
return result.IsSuccessStatusCode;
}
catch
{
return false;
}
}
private async Task<Version> GetPreviewerVersion()
{
using var result = await HttpClient.GetAsync("/version");
return await result.Content.ReadFromJsonAsync<Version>();
}
private void StartPreviewer()
{
try
{
var process = new Process
{
StartInfo = new()
{
FileName = "questpdf-previewer",
Arguments = $"{Port}",
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
Task.Run(async () =>
{
await process.WaitForExitAsync();
process.Dispose();
OnPreviewerStopped?.Invoke();
});
}
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 void CheckVersionCompatibility(Version version)
{
if (version.Major == 2022 && version.Minor == 5)
return;
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 2022.5'");
}
private async Task WaitForConnection()
{
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
var cancellationToken = cancellationTokenSource.Token;
while (true)
{
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;
}
}
public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
{
using var multipartContent = new MultipartFormDataContent();
var pages = new List<PreviewerRefreshCommand.Page>();
foreach (var picture in pictures)
{
var page = new PreviewerRefreshCommand.Page
{
Width = picture.Size.Width,
Height = picture.Size.Height
};
pages.Add(page);
var pictureStream = picture.Picture.Serialize().AsStream();
multipartContent.Add(new StreamContent(pictureStream), page.Id, page.Id);
}
var command = new PreviewerRefreshCommand
{
Pages = pages
};
multipartContent.Add(JsonContent.Create(command), "command");
using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
foreach (var picture in pictures)
picture.Picture.Dispose();
}
}
}
#endif
+11 -3
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.3.0</Version>
<Version>2022.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.</PackageDescription>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion>
@@ -18,13 +18,21 @@
<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>
<TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0</TargetFrameworks>
<TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0;net6.0</TargetFrameworks>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp" Version="2.80.*" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
<InternalsVisibleTo Include="QuestPDF.ReportSample" />
<InternalsVisibleTo Include="QuestPDF.UnitTests" />
<InternalsVisibleTo Include="QuestPDF.Examples" />
<InternalsVisibleTo Include="QuestPDF.Previewer" />
</ItemGroup>
<ItemGroup>
+70 -199
View File
@@ -1,222 +1,93 @@
## QuestPDF Overview
[![Dotnet](https://img.shields.io/badge/platform-.NET-blue)](https://www.nuget.org/packages/QuestPDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/QuestPDF/QuestPDF)](https://github.com/QuestPDF/QuestPDF/stargazers)
[![Nuget version](https://img.shields.io/nuget/v/QuestPdf)](https://www.nuget.org/packages/QuestPDF/)
[![Nuget download](https://img.shields.io/nuget/dt/QuestPDF)](https://www.nuget.org/packages/QuestPDF/)
[![License](https://img.shields.io/github/license/QuestPDF/QuestPDF)](https://github.com/QuestPDF/QuestPDF/blob/main/LICENSE)
[![Sponsor project](https://img.shields.io/badge/sponsor-project-red)](https://github.com/sponsors/QuestPDF)
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.
QuestPDF is an open-source .NET library for PDF documents generation.
## Features
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.
**Rely on solid fundamentals** - This library is created specifically for designing and arranging document layouts, with full paging support. Alternative solutions, such as HTML-based converters, are not designed for this purpose and therefore are often unpredictable and do not produce desired results.
## Installation
**Work with organized self-explanatory code** - The entire process of implementing PDF document, takes place in your code. Free yourself from slow visual designers and strange technological limitations. Follow simple yet highly effective approaches to create maintainable, high-quality code.
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/).
**Compose simple components into complex documents** - Do you remember the feeling when your code just works? When your ideas are becoming real without any effort? Working with simple, easy to understand, self-explanatory and highly composable layout elements is the key here!
```xml
// Package Manager
Install-Package QuestPDF
**Create and reuse components** - Feel no fear of complex documents! Create custom, reusable components and divide the document's layout into easy to maintain pieces. Inject data to customize content and use slots to enhance composability. Decide how complex approaches your solution needs and follow the best path.
// .NET CLI
dotnet add package QuestPDF
**Prototype with ease** - We understand that document generation is often tricky and require multiple iterations. The library offers additional prototyping tools such as random text generator or image placeholder element. By following best practices, you can develop a document without having data.
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.3.0" />
```
**Enjoy fast PDF generation** - QuestPDF is created upon SkiaSharp, a well-known graphical library, and converts your data into PDF documents. It offers a highly optimized layouting engine capable of generating over 1000 PDF files per minute per core. The entire process is thread-safe.
## Documentation
## Learning resources
**[Release notes and roadmap](https://www.questpdf.com/documentation/releases.html)** - everything that is planned for future library iterations, description of new features and information about potential breaking changes.
**[Getting started tutorial](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.
**[API Reference](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.
**[Patterns and practices](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)** - everything that may help you designing great reports and reusable code that is easy to maintain.
## Example invoice
Do you believe that creating a complete invoice document can take less than 200 lines of code? We have prepared for you a step-by-step instruction that shows every detail of this implementation and describes the best patterns and practices.
For tutorial, documentation and API reference, please visit [the QuestPDF documentation](https://www.questpdf.com/documentation/getting-started.html).
![invoice](https://raw.githubusercontent.com/QuestPDF/example-invoice/main/images/invoice.png)
Here you can find an example code showing how easy is to write and understand the fluent API.
[![Getting started tutorial]( https://img.shields.io/badge/%F0%9F%9A%80%20read-getting%20started-blue)](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.
**General document structure** with header, content and footer:
[![API reference](https://img.shields.io/badge/%F0%9F%93%96%20read-API%20reference-blue)](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.
```csharp
public void Compose(IDocumentContainer container)
[![Patterns and Practices](https://img.shields.io/badge/%F0%9F%94%8D%20read-patterns%20and%20practices-blue)](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.Margin(50);
page.Header().Element(ComposeHeader);
page.Content().Element(ComposeContent);
page.Footer().AlignCenter().Text(x =>
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();
x.Span(" / ");
x.TotalPages();
});
});
}
```
**The header area** consists of basic invoice information along with a logo placeholder.
```csharp
void ComposeHeader(IContainer container)
{
var titleTextStyle = TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium);
container.Row(row =>
{
{
column.Item().Text($"Invoice #{Model.InvoiceNumber}", titleStyle);
column.Item().Text(text =>
{
text.Span("Issue date: ", TextStyle.Default.SemiBold());
text.Span($"{Model.IssueDate:d}");
});
column.Item().Text(text =>
{
text.Span("Due date: ", TextStyle.Default.SemiBold());
text.Span($"{Model.DueDate:d}");
});
});
row.ConstantColumn(100).Height(50).Placeholder();
});
}
})
.GeneratePdf("hello.pdf");
```
And compare it to the produced PDF file:
Implementation of **the content area** that contains seller and customer details, then listing of all bought products, then a comments section.
![invoice](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/minimal-example-shadow.png)
```csharp
void ComposeContent(IContainer container)
{
container.PaddingVertical(40).column(column =>
{
column.Spacing(20);
column.Item().Row(row =>
{
row.RelativeColumn().Component(new AddressComponent("From", Model.SellerAddress));
row.ConstantColumn(50);
row.RelativeColumn().Component(new AddressComponent("For", Model.CustomerAddress));
});
## Are you ready for more?
column.Item().Element(ComposeTable);
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).
var totalPrice = Model.Items.Sum(x => x.Price * x.Quantity);
column
.Item()
.PaddingRight(5)
.AlignRight()
.Text($"Grand total: {totalPrice}$", TextStyle.Default.SemiBold());
if (!string.IsNullOrWhiteSpace(Model.Comments))
column.Item().PaddingTop(25).Element(ComposeComments);
});
}
```
**The table and comments** codes are extracted into separate methods to increase clarity:
```csharp
void ComposeTable(IContainer container)
{
var headerStyle = TextStyle.Default.SemiBold();
container.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(25);
columns.RelativeColumn(3);
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
table.Header(header =>
{
header.Cell().Text("#", headerStyle);
header.Cell().Text("Product", headerStyle);
header.Cell().AlignRight().Text("Unit price", headerStyle);
header.Cell().AlignRight().Text("Quantity", headerStyle);
header.Cell().AlignRight().Text("Total", headerStyle);
header.Cell().ColumnSpan(5)
.PaddingVertical(5).BorderBottom(1).BorderColor(Colors.Black);
});
foreach (var item in Model.Items)
{
table.Cell().Element(CellStyle).Text(Model.Items.IndexOf(item) + 1);
table.Cell().Element(CellStyle).Text(item.Name);
table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price}$");
table.Cell().Element(CellStyle).AlignRight().Text(item.Quantity);
table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price * item.Quantity}$");
static IContainer CellStyle(IContainer container)
{
container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5);
}
}
});
}
```
```csharp
void ComposeComments(IContainer container)
{
container.ShowEntire().Background(Colors.Grey.Lighten3).Padding(10).column(message =>
{
message.Spacing(5);
message.Item().Text("Comments", TextStyle.Default.Size(14).SemiBold());
message.Item().Text(Model.Comments);
});
}
```
**The address details section** is implemented using components. This way the code can be easily reused for both seller and customer:
```csharp
public class AddressComponent : IComponent
{
private string Title { get; }
private Address Address { get; }
public AddressComponent(string title, Address address)
{
Title = title;
Address = address;
}
public void Compose(IContainer container)
{
container.ShowEntire().column(column =>
{
column.Spacing(5);
column
.Item()
.BorderBottom(1)
.PaddingBottom(5)
.Text(Title, TextStyle.Default.SemiBold());
column.Item().Text(Address.CompanyName);
column.Item().Text(Address.Street);
column.Item().Text($"{Address.City}, {Address.State}");
column.Item().Text(Address.Email);
column.Item().Text(Address.Phone);
});
}
}
```
![invoice](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/invoice-small.png)
+4 -8
View File
@@ -1,8 +1,4 @@
- Added a `ScaleToFit` element - scales its child down so it fits in the provided space,
- Added a `StopPaging` element - when its child requires more than one page to fully render, only the first page is shown,
- Added a 'LineVertical' and a 'LineHorizontal' elements - those will simplify your code a lot, there is no need to use the `Border` element anymore!
- Renaming: the `Stack` element was renamed to the `Column` element,
- Renaming: children of the `Row` element are now called `items` instead of `columns`, e.g. `RelativeItem` instead of `RelativeColumn`,
- Added support of the `AutoItem` to the `Row` element - those items take as little width as possible,
- Improved default Fluent configuration behavior for elements: Scale, Padding, Translate,
- Improved integration support with the HttpContext.Response.Body. This improvement was introduced by schulz3000, thank you!
- Implemented the DynamicComponent element (useful for most advanced cases, e.g. per-page totals),
- Extend text rendering capabilities by adding subscript and superscript effects,
- Improved table rendering performance,
- Previewer tool stability fixes.
+61 -17
View File
@@ -1,23 +1,51 @@
<a href="https://www.questpdf.com/">
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/logo.svg" width="400px">
<a href="https://www.questpdf.com/" target="_blank">
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/logo.svg" width="300">
</a>
QuestPDF presents a new approach to PDF document generation. Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in many cases is not reliable. Instead, it implements its own layouting engine that is optimized to cover all paging-related requirements. Then, everything is rendered using the SkiaSharp library (a Skia port for .NET, used in Chrome, Android, MAUI, etc.).
---
I have designed this layouting engine with full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behaviour of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows.
[![Dotnet](https://img.shields.io/badge/platform-.NET-blue)](https://www.nuget.org/packages/QuestPDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/QuestPDF/QuestPDF)](https://github.com/QuestPDF/QuestPDF/stargazers)
[![Nuget version](https://img.shields.io/nuget/v/QuestPdf)](https://www.nuget.org/packages/QuestPDF/)
[![Nuget download](https://img.shields.io/nuget/dt/QuestPDF)](https://www.nuget.org/packages/QuestPDF/)
[![License](https://img.shields.io/github/license/QuestPDF/QuestPDF)](https://github.com/QuestPDF/QuestPDF/blob/main/LICENSE)
[![Sponsor project](https://img.shields.io/badge/sponsor-project-red)](https://github.com/sponsors/QuestPDF)
## Support QuestPDF
QuestPDF is an open-source .NET library for PDF documents generation.
All great frameworks and libraries started from zero. Please help me make QuestPDF a commonly known library and an obvious choice for generating PDF documents.
It offers a layouting 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.
- ⭐ Give this repository a star,
- 💬 Share it with your team members.
Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in many cases is not reliable. Instead, it implements its own layouting engine that is optimized to cover all paging-related requirements.
## Please show the value
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! ⭐
## 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!
[![GitHub Repo stars](https://img.shields.io/badge/share%20on-reddit-red?logo=reddit)](https://reddit.com/submit?url=https://github.com/QuestPDF/QuestPDF&title=QuestPDF)
[![GitHub Repo stars](https://img.shields.io/badge/share%20on-hacker%20news-orange?logo=ycombinator)](https://news.ycombinator.com/submitlink?u=https://github.com/QuestPDF/QuestPDF)
[![GitHub Repo stars](https://img.shields.io/badge/share%20on-twitter-03A9F4?logo=twitter)](https://twitter.com/share?url=https://github.com/QuestPDF/QuestPDF&t=QuestPDF)
[![GitHub Repo stars](https://img.shields.io/badge/share%20on-facebook-1976D2?logo=facebook)](https://www.facebook.com/sharer/sharer.php?u=https://github.com/QuestPDF/QuestPDF)
[![GitHub Repo stars](https://img.shields.io/badge/share%20on-linkedin-3949AB?logo=linkedin)](https://www.linkedin.com/shareArticle?url=https://github.com/QuestPDF/QuestPDF&title=QuestPDF)
## Support development
It doesn't matter if you are a professional developer, creating a startup or work for an established company. All of us care about our tools and dependencies, about stability and security, about time and money we can safe, about quality we can offer. Please consider sponsoring QuestPDF to give me an extra motivational push to develop the next great feature.
> If you represent a company, want to help the entire community and show that you care, please consider sponsoring QuestPDF using one of the higher tiers. Your company logo will be shown here for all developers, building a strong positive relation.
[![Sponsor project](https://img.shields.io/badge/%E2%9D%A4%EF%B8%8F%20sponsor-QuestPDF-red)](https://github.com/sponsors/QuestPDF)
## 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/).
```c#
```xml
// Package Manager
Install-Package QuestPDF
@@ -25,16 +53,31 @@ Install-Package QuestPDF
dotnet add package QuestPDF
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.2.5" />
<PackageReference Include="QuestPDF" Version="2022.4.1" />
```
[![Nuget version](https://img.shields.io/badge/package%20details-QuestPDF-blue?logo=nuget)](https://www.nuget.org/packages/QuestPDF/)
## Documentation
**[🚀 Getting started tutorial](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.
[![Getting started tutorial]( https://img.shields.io/badge/%F0%9F%9A%80%20read-getting%20started-blue)](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.
**[📖 API Reference](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.
**[️ Patterns and practices](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.
[![API reference](https://img.shields.io/badge/%F0%9F%93%96%20read-API%20reference-blue)](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.
[![Patterns and Practices](https://img.shields.io/badge/%E2%9C%A8%20read-patterns%20and%20practices-blue)](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.
## QuestPDF Previewer
The QuestPDF Previewer is a tool designed to simplify and speed up your development lifecycle. First, it shows a preview of your document. But the real magic starts with the hot-reload capability! It observes your code and updates the preview every time you change the implementation. Get real-time results without the need of code recompilation. Save time and enjoy the task!
[![Learn more](https://img.shields.io/badge/%F0%9F%93%96%20Previewer-learn%20more-blue)](https://www.questpdf.com/documentation/document-previewer.html)
<img src="https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/previewer/animation.gif" width="100%">
## Simplicity is the key
@@ -53,10 +96,11 @@ Document.Create(container =>
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Background(Colors.White);
page.DefaultTextStyle(TextStyle.Default.Size(20));
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hello PDF!", TextStyle.Default.SemiBold().Size(36).Color(Colors.Blue.Medium));
.Text("Hello PDF!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
@@ -82,10 +126,10 @@ Document.Create(container =>
And compare it to the produced PDF file:
<img src="https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/minimal-api.png" width="250px">
## 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).
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/invoice.png" width="595px">
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/invoice.png" width="400px">