Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4103797aa | |||
| d894914e7e | |||
| 90d1ce7af7 | |||
| 5fa32a06cc | |||
| acdf1ed1aa | |||
| a0b32e5ecf | |||
| d97fa2cef1 | |||
| 6d122c784d | |||
| 28743a81d1 | |||
| 32b94cea43 | |||
| 0ca55a91e0 | |||
| 09e6b5253f | |||
| bacb136a3f | |||
| b7bc640e31 | |||
| 6e6f884a5f | |||
| b47a8ac409 | |||
| e51082400c | |||
| 52cfa3ab7e | |||
| cd181b1e2e | |||
| e71601ad0c | |||
| 3041aa53cc | |||
| 0ebf2833af | |||
| 25e6d9fbca | |||
| f80149792d | |||
| 2beee1d22e | |||
| 1af7c0a144 | |||
| eeca67e5bc | |||
| 4da3ef5dfa | |||
| f3d4151273 | |||
| 9a7e397db2 | |||
| d81f21e658 | |||
| 78909c6edf | |||
| a2d81b6adf | |||
| 4a761c023f | |||
| 24bbd06dff | |||
| 44bd646bab | |||
| 5f0291b2c4 | |||
| 1b91015d8f | |||
| 2b6444463d | |||
| a2629575e8 | |||
| c8824021cc | |||
| e1101d870d | |||
| 152304b467 | |||
| 8a1be90c4f | |||
| bc8a9e5dc5 | |||
| e54e09de15 | |||
| c49a424f87 | |||
| c63b0658f3 | |||
| 82cd1d7835 | |||
| 3ffab6e933 | |||
| e4d55e0298 | |||
| a6f4fd7f49 | |||
| 048a4748b2 | |||
| 8cc2fb8696 | |||
| 8e7b528cd9 | |||
| 26193313b8 | |||
| 555825e8be | |||
| ddde8e7517 | |||
| d2c82b5988 | |||
| d82aae95b5 | |||
| 5c11a2ab6a | |||
| 03e3a80fd9 | |||
| 4692c5ce62 | |||
| 919f916219 | |||
| 7a32f774a1 | |||
| fba71e6392 | |||
| e27585a504 | |||
| 9be497ee41 | |||
| 0fc0db55d2 | |||
| 9e7a1ed49d | |||
| c137bcfcde | |||
| b80b3414d8 | |||
| 5f7cee3420 | |||
| 435c42ab9e | |||
| 7b4923b45d | |||
| 8732b7e50b | |||
| 6b31322f92 | |||
| 6155d21207 | |||
| a0909eb057 | |||
| 11ff53946d | |||
| 0711bca1e1 | |||
| 297ca48c70 | |||
| d2601e9e5a | |||
| 421a926780 | |||
| 307759d1b3 | |||
| 00bcb2c25b | |||
| e430484e85 | |||
| d24d887a3a | |||
| 404914ea7e | |||
| 545b76fd40 | |||
| c801894ec4 | |||
| c21744e7de | |||
| 734d116bfe | |||
| 0abb67b748 | |||
| 828bb86737 | |||
| 040c47e017 | |||
| ff95fe287a | |||
| 47e9a49fe5 | |||
| b2826a06b8 | |||
| 321cba32ce | |||
| 26fdbb714d | |||
| f0cec8988a | |||
| ea2102fe4c | |||
| 6cca3ed057 | |||
| 9afe4ec104 | |||
| 846679007f | |||
| 0946607154 | |||
| d5f86e0d70 | |||
| 346513ee85 | |||
| d18ee5f282 | |||
| a85bc0abc1 | |||
| 82e39df572 | |||
| 1311b6db32 | |||
| b5645bfbd5 | |||
| 8217b3b54a | |||
| 726759f47a | |||
| bbd9c3379e | |||
| 697c5e238d | |||
| 21a92be87e | |||
| b0cca1ff48 | |||
| 78c5049eda | |||
| 3d0ca07b43 | |||
| a67fa754af | |||
| 208235ad1e | |||
| f24416474a | |||
| 01c2e8d3f5 | |||
| b3cef238ec | |||
| aede62c135 | |||
| db9db95dcc | |||
| 92263685d7 | |||
| ba4be521aa | |||
| f6967dfe8b | |||
| ff3dfe7831 | |||
| 5a1990d857 | |||
| 1b9b164583 | |||
| 9c64a6bc8d | |||
| 5402e5fedc | |||
| 2c14f91ebe | |||
| b533a32b61 | |||
| 262bd3c182 | |||
| fdf8e3eb1a | |||
| fcff989ab2 | |||
| 456c7a735b | |||
| 5bf2a9a3d3 | |||
| 69e268733c | |||
| 657bbe2aac |
+131
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
github: QuestPDF
|
||||
@@ -13,7 +13,7 @@ namespace QuestPDF.Examples
|
||||
[Test]
|
||||
public void Example()
|
||||
{
|
||||
FontManager.RegisterFontType(File.OpenRead("LibreBarcode39-Regular.ttf"));
|
||||
FontManager.RegisterFont(File.OpenRead("LibreBarcode39-Regular.ttf"));
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
@@ -25,7 +25,9 @@ namespace QuestPDF.Examples
|
||||
.Background(Colors.White)
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Text("*QuestPDF*", TextStyle.Default.FontType("Libre Barcode 39").Size(64));
|
||||
.Text("*QuestPDF*")
|
||||
.FontFamily("Libre Barcode 39")
|
||||
.FontSize(64);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,10 @@ namespace QuestPDF.Examples
|
||||
.PrimaryLayer()
|
||||
.PaddingVertical(10)
|
||||
.PaddingHorizontal(20)
|
||||
.Text("Sample text", TextStyle.Default.Size(16).Color(Colors.Blue.Darken2).SemiBold());
|
||||
.Text("Sample text")
|
||||
.FontSize(16)
|
||||
.FontColor(Colors.Blue.Darken2)
|
||||
.SemiBold();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,7 +57,10 @@ namespace QuestPDF.Examples
|
||||
column
|
||||
.Item()
|
||||
.PaddingBottom(10)
|
||||
.Text("Chart example", TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium));
|
||||
.Text("Chart example")
|
||||
.FontSize(20)
|
||||
.SemiBold()
|
||||
.FontColor(Colors.Blue.Medium);
|
||||
|
||||
column
|
||||
.Item()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace QuestPDF.Examples
|
||||
page.Content().PaddingVertical(10).Border(1).Padding(10).Column(column =>
|
||||
{
|
||||
foreach (var index in Enumerable.Range(1, 100))
|
||||
column.Item().Text($"Line {index}", TextStyle.Default.Color(Placeholders.Color()));
|
||||
column.Item().Text($"Line {index}").FontColor(Placeholders.Color());
|
||||
});
|
||||
|
||||
page.Footer().Text("Footer");
|
||||
|
||||
@@ -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 =>
|
||||
{
|
||||
@@ -38,7 +38,7 @@ namespace QuestPDF.Examples
|
||||
text.Line(Placeholders.Sentence());
|
||||
|
||||
// this text has size 20 but also semibold and red
|
||||
text.Span(Placeholders.Sentence(), TextStyle.Default.Color(Colors.Red.Medium));
|
||||
text.Span(Placeholders.Sentence()).FontColor(Colors.Red.Medium);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,8 +26,8 @@ namespace QuestPDF.Examples
|
||||
.DefaultTextStyle(TextStyle.Default.Bold().Underline())
|
||||
.Column(column =>
|
||||
{
|
||||
column.Item().Text("Default style applies to all children", TextStyle.Default);
|
||||
column.Item().Text("You can override certain styles", TextStyle.Default.Underline(false).Color(Colors.Green.Darken2));
|
||||
column.Item().Text("Default style applies to all children");
|
||||
column.Item().Text("You can override certain styles").Underline(false).FontColor(Colors.Green.Darken2);
|
||||
|
||||
column.Item().PaddingTop(10).Border(1).Grid(grid =>
|
||||
{
|
||||
@@ -43,7 +43,8 @@ namespace QuestPDF.Examples
|
||||
.Height(50)
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Text(i, TextStyle.Default.Size(16 + i / 4));
|
||||
.Text(i)
|
||||
.FontSize(16 + i / 4);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
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 struct FibonacciHeaderState
|
||||
{
|
||||
public int Previous { get; set; }
|
||||
public int Current { get; set; }
|
||||
}
|
||||
|
||||
public class FibonacciHeader : IDynamicComponent<FibonacciHeaderState>
|
||||
{
|
||||
public FibonacciHeaderState State { get; set; }
|
||||
|
||||
public static readonly string[] ColorsTable =
|
||||
{
|
||||
Colors.Red.Lighten2,
|
||||
Colors.Orange.Lighten2,
|
||||
Colors.Green.Lighten2,
|
||||
};
|
||||
|
||||
public FibonacciHeader(int previous, int current)
|
||||
{
|
||||
State = new FibonacciHeaderState
|
||||
{
|
||||
Previous = previous,
|
||||
Current = current
|
||||
};
|
||||
}
|
||||
|
||||
public DynamicComponentComposeResult Compose(DynamicContext context)
|
||||
{
|
||||
var content = context.CreateElement(container =>
|
||||
{
|
||||
var colorIndex = State.Current % ColorsTable.Length;
|
||||
var color = ColorsTable[colorIndex];
|
||||
|
||||
var ratio = (float)State.Current / State.Previous;
|
||||
|
||||
container
|
||||
.Background(color)
|
||||
.Height(50)
|
||||
.AlignMiddle()
|
||||
.AlignCenter()
|
||||
.Text($"{State.Current} / {State.Previous} = {ratio:N5}");
|
||||
});
|
||||
|
||||
State = new FibonacciHeaderState
|
||||
{
|
||||
Previous = State.Current,
|
||||
Current = State.Previous + State.Current
|
||||
};
|
||||
|
||||
return new DynamicComponentComposeResult
|
||||
{
|
||||
Content = content,
|
||||
HasMoreContent = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class DynamicFibonacci
|
||||
{
|
||||
[Test]
|
||||
public static void Dynamic()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ShowResults()
|
||||
.MaxPages(100)
|
||||
.ProduceImages()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A6);
|
||||
page.PageColor(Colors.White);
|
||||
page.Margin(1, Unit.Centimetre);
|
||||
page.DefaultTextStyle(x => x.FontSize(18));
|
||||
|
||||
page.Header().Dynamic(new FibonacciHeader(17, 19));
|
||||
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
foreach (var i in Enumerable.Range(0, 50))
|
||||
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
|
||||
});
|
||||
|
||||
page.Footer().AlignCenter().Text(text =>
|
||||
{
|
||||
text.CurrentPageNumber();
|
||||
text.Span(" / ");
|
||||
text.TotalPages();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 OptimizedOrdersTable : IDynamicComponent<OrdersTableState>
|
||||
{
|
||||
private ICollection<OrderItem> Items { get; }
|
||||
public OrdersTableState State { get; set; }
|
||||
|
||||
public OptimizedOrdersTable(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 < 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 DynamicOptimizedExamples
|
||||
{
|
||||
[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 OptimizedOrdersTable(items));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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 FooterWithAlternatingAlignment : IDynamicComponent<int>
|
||||
{
|
||||
public int State { get; set; }
|
||||
|
||||
public DynamicComponentComposeResult Compose(DynamicContext context)
|
||||
{
|
||||
var content = context.CreateElement(element =>
|
||||
{
|
||||
element
|
||||
.Element(x => context.PageNumber % 2 == 0 ? x.AlignLeft() : x.AlignRight())
|
||||
.Text(x =>
|
||||
{
|
||||
x.CurrentPageNumber();
|
||||
x.Span(" / ");
|
||||
x.TotalPages();
|
||||
});
|
||||
});
|
||||
|
||||
return new DynamicComponentComposeResult()
|
||||
{
|
||||
Content = content,
|
||||
HasMoreContent = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class DynamicPageNumberLeftRightExamples
|
||||
{
|
||||
[Test]
|
||||
public static void Dynamic()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(PageSizes.A5)
|
||||
.MaxPages(100)
|
||||
.ShowResults()
|
||||
.ProduceImages()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A6);
|
||||
page.PageColor(Colors.White);
|
||||
page.Margin(1, Unit.Centimetre);
|
||||
page.DefaultTextStyle(x => x.FontSize(18));
|
||||
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
foreach (var i in Enumerable.Range(0, 50))
|
||||
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
|
||||
});
|
||||
|
||||
page.Footer().Dynamic(new FooterWithAlternatingAlignment());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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 ProgressHeader : IDynamicComponent<int>
|
||||
{
|
||||
public int State { get; set; }
|
||||
|
||||
public DynamicComponentComposeResult Compose(DynamicContext context)
|
||||
{
|
||||
var content = context.CreateElement(container =>
|
||||
{
|
||||
var width = context.AvailableSize.Width * context.PageNumber / context.TotalPages;
|
||||
|
||||
container
|
||||
.Background(Colors.Blue.Lighten2)
|
||||
.Height(25)
|
||||
.Width(width)
|
||||
.Background(Colors.Blue.Darken1);
|
||||
});
|
||||
|
||||
return new DynamicComponentComposeResult
|
||||
{
|
||||
Content = content,
|
||||
HasMoreContent = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class DynamicProgressHeader
|
||||
{
|
||||
[Test]
|
||||
public static void Dynamic()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ShowResults()
|
||||
.MaxPages(100)
|
||||
.ProduceImages()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A6);
|
||||
page.Margin(1, Unit.Centimetre);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header().Dynamic(new ProgressHeader());
|
||||
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
foreach (var i in Enumerable.Range(0, 100))
|
||||
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
|
||||
});
|
||||
|
||||
page.Footer().AlignCenter().Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
text.CurrentPageNumber();
|
||||
text.Span(" / ");
|
||||
text.TotalPages();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
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 OrdersTable : IDynamicComponent<OrdersTableState>
|
||||
{
|
||||
private IList<OrderItem> Items { get; }
|
||||
public OrdersTableState State { get; set; }
|
||||
|
||||
public OrdersTable(IList<OrderItem> items)
|
||||
{
|
||||
Items = items;
|
||||
|
||||
State = new OrdersTableState
|
||||
{
|
||||
ShownItemsCount = 0
|
||||
};
|
||||
}
|
||||
|
||||
public DynamicComponentComposeResult Compose(DynamicContext context)
|
||||
{
|
||||
var possibleItems = Enumerable
|
||||
.Range(1, Items.Count - State.ShownItemsCount)
|
||||
.Select(itemsToDisplay => ComposeContent(context, itemsToDisplay))
|
||||
.TakeWhile(x => x.Size.Height <= context.AvailableSize.Height)
|
||||
.ToList();
|
||||
|
||||
State = new OrdersTableState
|
||||
{
|
||||
ShownItemsCount = State.ShownItemsCount + possibleItems.Count
|
||||
};
|
||||
|
||||
return new DynamicComponentComposeResult
|
||||
{
|
||||
Content = possibleItems.Last(),
|
||||
HasMoreContent = State.ShownItemsCount < Items.Count
|
||||
};
|
||||
}
|
||||
|
||||
private IDynamicElement ComposeContent(DynamicContext context, int itemsToDisplay)
|
||||
{
|
||||
var total = Items.Skip(State.ShownItemsCount).Take(itemsToDisplay).Sum(x => x.Count * x.Price);
|
||||
|
||||
return context.CreateElement(container =>
|
||||
{
|
||||
container
|
||||
.MinimalBox()
|
||||
.Width(context.AvailableSize.Width)
|
||||
.Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
{
|
||||
columns.ConstantColumn(30);
|
||||
columns.RelativeColumn();
|
||||
columns.ConstantColumn(50);
|
||||
columns.ConstantColumn(50);
|
||||
columns.ConstantColumn(50);
|
||||
});
|
||||
|
||||
table.Header(header =>
|
||||
{
|
||||
header.Cell().Element(Style).Text("#");
|
||||
header.Cell().Element(Style).Text("Item name");
|
||||
header.Cell().Element(Style).AlignRight().Text("Count");
|
||||
header.Cell().Element(Style).AlignRight().Text("Price");
|
||||
header.Cell().Element(Style).AlignRight().Text("Total");
|
||||
|
||||
IContainer Style(IContainer container)
|
||||
{
|
||||
return container
|
||||
.DefaultTextStyle(x => x.SemiBold())
|
||||
.BorderBottom(1)
|
||||
.BorderColor(Colors.Grey.Darken2)
|
||||
.Padding(5);
|
||||
}
|
||||
});
|
||||
|
||||
table.Footer(footer =>
|
||||
{
|
||||
footer
|
||||
.Cell().ColumnSpan(5)
|
||||
.AlignRight()
|
||||
.PaddingTop(10)
|
||||
.Text($"Subtotal: {total}$", TextStyle.Default.Bold());
|
||||
});
|
||||
|
||||
foreach (var index in Enumerable.Range(State.ShownItemsCount, itemsToDisplay))
|
||||
{
|
||||
var item = Items[index];
|
||||
|
||||
table.Cell().Element(Style).Text(index + 1);
|
||||
table.Cell().Element(Style).Text(item.ItemName);
|
||||
table.Cell().Element(Style).AlignRight().Text(item.Count);
|
||||
table.Cell().Element(Style).AlignRight().Text($"{item.Price}$");
|
||||
table.Cell().Element(Style).AlignRight().Text($"{item.Count*item.Price}$");
|
||||
|
||||
IContainer Style(IContainer container)
|
||||
{
|
||||
return container
|
||||
.BorderBottom(1)
|
||||
.BorderColor(Colors.Grey.Lighten2)
|
||||
.Padding(5);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class DynamicSimpleTableExample
|
||||
{
|
||||
[Test]
|
||||
public static void Dynamic()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(PageSizes.A5)
|
||||
.ShowResults()
|
||||
.ProduceImages()
|
||||
.Render(container =>
|
||||
{
|
||||
var items = Enumerable.Range(0, 15).Select(x => new OrderItem()).ToList();
|
||||
|
||||
container
|
||||
.Background(Colors.White)
|
||||
.Padding(25)
|
||||
.DefaultTextStyle(x => x.FontSize(16))
|
||||
.Decoration(decoration =>
|
||||
{
|
||||
decoration
|
||||
.Header()
|
||||
.PaddingBottom(5)
|
||||
.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2));
|
||||
text.Span("Page ");
|
||||
text.CurrentPageNumber();
|
||||
text.Span(" / ");
|
||||
text.TotalPages();
|
||||
});
|
||||
|
||||
decoration
|
||||
.Content()
|
||||
.Dynamic(new OrdersTable(items));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,9 @@ namespace QuestPDF.Examples
|
||||
.Before()
|
||||
.Background(Colors.Grey.Medium)
|
||||
.Padding(10)
|
||||
.Text("Notes", TextStyle.Default.Size(16).Color("#FFF"));
|
||||
.Text("Notes")
|
||||
.FontSize(16)
|
||||
.FontColor("#FFF");
|
||||
|
||||
decoration
|
||||
.Content()
|
||||
@@ -280,12 +282,15 @@ namespace QuestPDF.Examples
|
||||
.Layer()
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Text("Watermark", TextStyle.Default.Size(48).Bold().Color(Colors.Green.Lighten3));
|
||||
.Text("Watermark")
|
||||
.FontSize(48)
|
||||
.Bold()
|
||||
.FontColor(Colors.Green.Lighten3);
|
||||
|
||||
layers
|
||||
.Layer()
|
||||
.AlignBottom()
|
||||
.Text(text => text.CurrentPageNumber(TextStyle.Default.Size(16).Color(Colors.Green.Medium)));
|
||||
.Text(text => text.CurrentPageNumber().FontSize(16).FontColor(Colors.Green.Medium));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -416,7 +421,9 @@ namespace QuestPDF.Examples
|
||||
.Border(1)
|
||||
.BorderColor(Colors.Grey.Medium)
|
||||
.Padding(10)
|
||||
.Text(font, TextStyle.Default.FontType(font).Size(16));
|
||||
.Text(font)
|
||||
.FontFamily(font)
|
||||
.FontSize(16);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -456,7 +463,7 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
|
||||
layers.Layer().Background("#8F00").Extend();
|
||||
layers.Layer().PaddingTop(40).Text("It works!", TextStyle.Default.Size(24));
|
||||
layers.Layer().PaddingTop(40).Text("It works!").FontSize(24);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -477,7 +484,7 @@ namespace QuestPDF.Examples
|
||||
//.MinimalBox()
|
||||
.Background(Colors.Grey.Lighten2)
|
||||
.Padding(15)
|
||||
.Text("Test of the \n box element", TextStyle.Default.Size(20));
|
||||
.Text("Test of the \n box element").FontSize(20);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -503,7 +510,8 @@ namespace QuestPDF.Examples
|
||||
decoration
|
||||
.Before()
|
||||
.PaddingBottom(10)
|
||||
.Text("Example: scale component", headerFontStyle);
|
||||
.Text("Example: scale component")
|
||||
.Style(headerFontStyle);
|
||||
|
||||
decoration
|
||||
.Content()
|
||||
@@ -525,7 +533,8 @@ namespace QuestPDF.Examples
|
||||
.Background(fontColor)
|
||||
.Scale(scale)
|
||||
.Padding(5)
|
||||
.Text($"Content with {scale} scale.", fontStyle);
|
||||
.Text($"Content with {scale} scale.")
|
||||
.Style(fontStyle);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -555,7 +564,8 @@ namespace QuestPDF.Examples
|
||||
.BorderColor(Colors.Green.Darken1)
|
||||
|
||||
.Padding(50)
|
||||
.Text("Moved text", TextStyle.Default.Size(25));
|
||||
.Text("Moved text")
|
||||
.FontSize(25);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -591,7 +601,8 @@ namespace QuestPDF.Examples
|
||||
.MinimalBox()
|
||||
.Background(Colors.White)
|
||||
.Padding(10)
|
||||
.Text($"Rotated {turns * 90}°", TextStyle.Default.Size(16));
|
||||
.Text($"Rotated {turns * 90}°")
|
||||
.FontSize(16);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -689,7 +700,8 @@ namespace QuestPDF.Examples
|
||||
.MinimalBox()
|
||||
.Background(Colors.White)
|
||||
.Padding(10)
|
||||
.Text($"Flipped {turns}", TextStyle.Default.Size(16));
|
||||
.Text($"Flipped {turns}")
|
||||
.FontSize(16);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -730,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ 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", TextStyle.Default.SemiBold());
|
||||
page.Header().Text("With ensure space").SemiBold();
|
||||
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
|
||||
@@ -17,9 +17,9 @@ namespace QuestPDF.Examples
|
||||
.Padding(5);
|
||||
}
|
||||
|
||||
public static void LabelCell(this IContainer container, string text) => container.Cell(true).Text(text, TextStyle.Default.SemiBold());
|
||||
public static void LabelCell(this IContainer container, string text) => container.Cell(true).Text(text).SemiBold();
|
||||
public static IContainer ValueCell(this IContainer container) => container.Cell(false);
|
||||
public static void ValueCell(this IContainer container, string text) => container.ValueCell().Text(text, TextStyle.Default);
|
||||
public static void ValueCell(this IContainer container, string text) => container.ValueCell().Text(text);
|
||||
}
|
||||
|
||||
public class FrameExample
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
public class GridBenchmark
|
||||
{
|
||||
[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)
|
||||
.Grid(grid =>
|
||||
{
|
||||
const int numberOfRows = 100_000;
|
||||
const int numberOfColumns = 10;
|
||||
|
||||
grid.Columns(numberOfColumns);
|
||||
|
||||
foreach (var row in Enumerable.Range(0, numberOfRows))
|
||||
foreach (var column in Enumerable.Range(0, numberOfColumns))
|
||||
grid.Item().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,8 @@ namespace QuestPDF.Examples
|
||||
.PrimaryLayer()
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Text(sizeText, TextStyle.Default.Size(15));
|
||||
.Text(sizeText)
|
||||
.FontSize(15);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,8 +4,6 @@ using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using ShimSkiaSharp;
|
||||
using SKTypeface = SkiaSharp.SKTypeface;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
@@ -19,21 +17,32 @@ namespace QuestPDF.Examples
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A5);
|
||||
page.Margin(1.5f, Unit.Centimetre);
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header()
|
||||
.Text("Hello PDF!", TextStyle.Default.SemiBold().Size(20));
|
||||
.Text("Hello PDF!")
|
||||
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(10);
|
||||
x.Spacing(20);
|
||||
|
||||
x.Item().Text(Placeholders.LoremIpsum());
|
||||
x.Item().Image(Placeholders.Image(200, 100));
|
||||
});
|
||||
|
||||
page.Footer()
|
||||
.AlignCenter()
|
||||
.Text(x =>
|
||||
{
|
||||
x.Span("Page ");
|
||||
x.CurrentPageNumber();
|
||||
});
|
||||
});
|
||||
})
|
||||
.GeneratePdf("hello.pdf");
|
||||
|
||||
@@ -50,7 +50,8 @@ namespace QuestPDF.Examples
|
||||
.Background("FFF")
|
||||
.Padding(5)
|
||||
.AlignCenter()
|
||||
.Text("Sample text", TextStyle.Default.FontType("Segoe UI emoji"));
|
||||
.Text("Sample text")
|
||||
.FontFamily("Segoe UI emoji");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
public class PageBackgroundForegroundExample
|
||||
{
|
||||
[Test]
|
||||
public void PageBackgroundForeground()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.MaxPages(100)
|
||||
.ShowResults()
|
||||
.RenderDocument(document =>
|
||||
{
|
||||
document.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(1, Unit.Inch);
|
||||
page.DefaultTextStyle(TextStyle.Default.FontSize(16));
|
||||
page.PageColor(Colors.White);
|
||||
|
||||
const string transparentBlue = "#662196f3";
|
||||
|
||||
page.Background()
|
||||
.AlignTop()
|
||||
.ExtendHorizontal()
|
||||
.Height(200)
|
||||
.Background(transparentBlue);
|
||||
|
||||
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 =>
|
||||
{
|
||||
column.Spacing(25);
|
||||
|
||||
foreach (var i in Enumerable.Range(0, 100))
|
||||
column.Item().Background(Colors.Grey.Lighten2).Height(75);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,9 @@ 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", TextStyle.Default.SemiBold());
|
||||
page.Header().Text("With show once").SemiBold();
|
||||
|
||||
page.Content().PaddingVertical(5).Row(row =>
|
||||
{
|
||||
|
||||
@@ -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 =>
|
||||
{
|
||||
@@ -31,7 +31,8 @@ namespace QuestPDF.Examples
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(10)
|
||||
.Text(Placeholders.Paragraphs(), TextStyle.Default.Color(Colors.Grey.Medium));
|
||||
.Text(Placeholders.Paragraphs())
|
||||
.FontColor(Colors.Grey.Medium);
|
||||
|
||||
page.Footer().Text(text =>
|
||||
{
|
||||
|
||||
@@ -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}");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace QuestPDF.Examples
|
||||
.PageSize(PageSizes.A4)
|
||||
.ProducePdf()
|
||||
.ShowResults()
|
||||
.Render(x => ComposeBook(x, chapters));
|
||||
.RenderDocument(x => ComposeBook(x, chapters));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -31,7 +31,7 @@ namespace QuestPDF.Examples
|
||||
{
|
||||
var chapters = GetBookChapters().ToList();
|
||||
|
||||
var results = PerformTest(16).ToList();
|
||||
var results = PerformTest(128).ToList();
|
||||
|
||||
Console.WriteLine($"Min: {results.Min():F}");
|
||||
Console.WriteLine($"Max: {results.Max():F}");
|
||||
@@ -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
|
||||
@@ -141,8 +134,8 @@ namespace QuestPDF.Examples
|
||||
.AlignBottom()
|
||||
.Column(column =>
|
||||
{
|
||||
column.Item().Text("Quo Vadis", TextStyle.Default.Size(72).Bold().Color(Colors.Blue.Darken2));
|
||||
column.Item().Text("Henryk Sienkiewicz", TextStyle.Default.Size(24).Color(Colors.Grey.Darken2));
|
||||
column.Item().Text("Quo Vadis").FontSize(72).Bold().FontColor(Colors.Blue.Darken2);
|
||||
column.Item().Text("Henryk Sienkiewicz").FontSize(24).FontColor(Colors.Grey.Darken2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,8 +149,8 @@ namespace QuestPDF.Examples
|
||||
{
|
||||
column.Item().InternalLink(chapter.Title).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text(chapter.Title, normalStyle);
|
||||
row.ConstantItem(100).AlignRight().Text(text => text.PageNumberOfLocation(chapter.Title, normalStyle));
|
||||
row.RelativeItem().Text(chapter.Title).Style(normalStyle);
|
||||
row.ConstantItem(100).AlignRight().Text(text => text.BeginPageNumberOfSection(chapter.Title).Style(normalStyle));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -180,7 +173,7 @@ namespace QuestPDF.Examples
|
||||
column.Item().Text(text =>
|
||||
{
|
||||
text.ParagraphSpacing(5);
|
||||
text.Span(content, normalStyle);
|
||||
text.Span(content).Style(normalStyle);
|
||||
});
|
||||
|
||||
column.Item().PageBreak();
|
||||
@@ -198,7 +191,7 @@ namespace QuestPDF.Examples
|
||||
text.DefaultTextStyle(normalStyle);
|
||||
|
||||
text.Span("Ten dokument został wygenerowany na podstawie książki w formacie TXT opublikowanej w serwisie ");
|
||||
text.ExternalLocation("wolnelektury.pl", "https://wolnelektury.pl/", normalStyle.Color(Colors.Blue.Medium).Underline());
|
||||
text.Hyperlink("wolnelektury.pl", "https://wolnelektury.pl/").FontColor(Colors.Blue.Medium).Underline();
|
||||
text.Span(". Dziękuję za wspieranie polskiego czytelnictwa!");
|
||||
});
|
||||
});
|
||||
@@ -206,7 +199,7 @@ namespace QuestPDF.Examples
|
||||
|
||||
void SectionTitle(ColumnDescriptor column, string text)
|
||||
{
|
||||
column.Item().Location(text).Text(text, subtitleStyle);
|
||||
column.Item().Location(text).Text(text).Style(subtitleStyle);
|
||||
column.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ namespace QuestPDF.Examples
|
||||
public class TextExamples
|
||||
{
|
||||
[Test]
|
||||
public void SimpleTextBlock()
|
||||
public void SimpleText()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(500, 300)
|
||||
.PageSize(500, 100)
|
||||
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
@@ -27,21 +27,192 @@ namespace QuestPDF.Examples
|
||||
.MinimalBox()
|
||||
.Border(1)
|
||||
.Padding(10)
|
||||
.Text(Placeholders.Paragraph());
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SimpleTextBlock()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(600, 300)
|
||||
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(5)
|
||||
.MinimalBox()
|
||||
.Border(1)
|
||||
.MaxWidth(300)
|
||||
.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.", TextStyle.Default.Size(20).Color(Colors.Red.Medium).Underline());
|
||||
text.Span("underlined red text").FontColor(Colors.Red.Medium).Underline();
|
||||
text.Span(".");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TextWeight()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(500, 500)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(20)
|
||||
.MinimalBox()
|
||||
.Border(1)
|
||||
.Padding(20)
|
||||
.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(x => x.FontFamily(Fonts.Calibri).FontSize(20));
|
||||
|
||||
text.Line("Thin").Thin();
|
||||
text.Line("ExtraLight").ExtraLight();
|
||||
text.Line("Light").Light();
|
||||
text.Line("NormalWeight").NormalWeight();
|
||||
text.Line("Medium").Medium();
|
||||
text.Line("SemiBold").SemiBold();
|
||||
text.Line("Bold").Bold();
|
||||
text.Line("ExtraBold").ExtraBold();
|
||||
text.Line("Black").Black();
|
||||
text.Line("ExtraBlack").ExtraBlack();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LineHeight()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(500, 700)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(20)
|
||||
.Column(column =>
|
||||
{
|
||||
var lineHeights = new[] { 0.8f, 1f, 1.5f };
|
||||
var paragraph = Placeholders.Paragraph();
|
||||
|
||||
foreach (var lineHeight in lineHeights)
|
||||
{
|
||||
column
|
||||
.Item()
|
||||
.Border(1)
|
||||
.Padding(10)
|
||||
.Text(paragraph)
|
||||
.FontSize(16)
|
||||
.LineHeight(lineHeight);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SuperscriptSubscript_Simple()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(500, 500)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(20)
|
||||
.MinimalBox()
|
||||
.Border(1)
|
||||
.Padding(20)
|
||||
.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(x => x.FontSize(20));
|
||||
text.ParagraphSpacing(10);
|
||||
|
||||
var highlight = TextStyle.Default.BackgroundColor(Colors.Green.Lighten3);
|
||||
|
||||
text.Span("E=mc").Style(highlight);
|
||||
text.Span("2").Superscript().Style(highlight);
|
||||
text.Span(" is the equation of mass–energy equivalence.");
|
||||
|
||||
text.EmptyLine();
|
||||
|
||||
text.Span("H").Style(highlight);
|
||||
text.Span("2").Subscript().Style(highlight);
|
||||
text.Span("O").Style(highlight);
|
||||
text.Span(" is the chemical formula for water.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(500, 300)
|
||||
.PageSize(500, 500)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
@@ -53,13 +224,7 @@ namespace QuestPDF.Examples
|
||||
.Padding(10)
|
||||
.Text(text =>
|
||||
{
|
||||
text.ParagraphSpacing(10);
|
||||
|
||||
foreach (var i in Enumerable.Range(1, 3))
|
||||
{
|
||||
text.Span($"Paragraph {i}: ", TextStyle.Default.SemiBold());
|
||||
text.Line(Placeholders.Paragraph());
|
||||
}
|
||||
text.Line(Placeholders.Paragraph());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -81,7 +246,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()
|
||||
@@ -120,7 +285,7 @@ namespace QuestPDF.Examples
|
||||
|
||||
text.Line(Placeholders.LoremIpsum());
|
||||
|
||||
text.Span($"This is target text that should show up. {DateTime.UtcNow:T} > This is a short sentence that will be wrapped into second line hopefully, right? <", TextStyle.Default.Underline());
|
||||
text.Span($"This is target text that should show up. {DateTime.UtcNow:T} > This is a short sentence that will be wrapped into second line hopefully, right? <").Underline();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -173,6 +338,8 @@ namespace QuestPDF.Examples
|
||||
.Padding(10)
|
||||
.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(x => x.Bold());
|
||||
|
||||
text.DefaultTextStyle(TextStyle.Default);
|
||||
text.AlignLeft();
|
||||
text.ParagraphSpacing(10);
|
||||
@@ -182,14 +349,14 @@ namespace QuestPDF.Examples
|
||||
text.EmptyLine();
|
||||
|
||||
text.Span("This text is a normal text, ");
|
||||
text.Span("this is a bold text, ", TextStyle.Default.Bold());
|
||||
text.Span("this is a red and underlined text, ", TextStyle.Default.Color(Colors.Red.Medium).Underline());
|
||||
text.Span("and this is slightly bigger text.", TextStyle.Default.Size(16));
|
||||
text.Span("this is a bold text, ").Bold();
|
||||
text.Span("this is a red and underlined text, ").FontColor(Colors.Red.Medium).Underline();
|
||||
text.Span("and this is slightly bigger text.").FontSize(16);
|
||||
|
||||
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();
|
||||
@@ -201,7 +368,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();
|
||||
|
||||
@@ -210,7 +377,7 @@ namespace QuestPDF.Examples
|
||||
|
||||
text.EmptyLine();
|
||||
|
||||
text.Span(Placeholders.Paragraphs(), TextStyle.Default.Italic());
|
||||
text.Span(Placeholders.Paragraphs()).Italic();
|
||||
|
||||
text.Line("This is target text that does not show up. " + Placeholders.Paragraph());
|
||||
});
|
||||
@@ -236,17 +403,17 @@ namespace QuestPDF.Examples
|
||||
.Padding(10)
|
||||
.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(TextStyle.Default);
|
||||
text.DefaultTextStyle(TextStyle.Default.FontSize(20).BackgroundColor(Colors.Red.Lighten4));
|
||||
text.AlignLeft();
|
||||
text.ParagraphSpacing(10);
|
||||
|
||||
text.Span("This text is a normal text, ");
|
||||
text.Span("this is a bold text, ", TextStyle.Default.Bold());
|
||||
text.Span("this is a red and underlined text, ", TextStyle.Default.Color(Colors.Red.Medium).Underline());
|
||||
text.Span("and this is slightly bigger text.", TextStyle.Default.Size(16));
|
||||
text.Span("this is a bold text, ").Bold();
|
||||
text.Span("this is a red and underlined text, ").FontColor(Colors.Red.Medium).Underline();
|
||||
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();
|
||||
@@ -255,7 +422,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();
|
||||
@@ -267,5 +434,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.HarfBuzz;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
public class TextShapingTests
|
||||
{
|
||||
// [Test]
|
||||
// public void ShapeText()
|
||||
// {
|
||||
// using var textPaint = new SKPaint
|
||||
// {
|
||||
// Color = SKColors.Black,
|
||||
// Typeface = SKTypeface.CreateDefault(),
|
||||
// IsAntialias = true,
|
||||
// TextSize = 20
|
||||
// };
|
||||
//
|
||||
// using var backgroundPaint = new SKPaint
|
||||
// {
|
||||
// Color = SKColors.LightGray
|
||||
// };
|
||||
//
|
||||
// RenderingTest
|
||||
// .Create()
|
||||
// .PageSize(550, 250)
|
||||
// .ProduceImages()
|
||||
// .ShowResults()
|
||||
// .Render(container =>
|
||||
// {
|
||||
// //var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus.";
|
||||
// //var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور";
|
||||
//
|
||||
// var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
|
||||
// var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
|
||||
//
|
||||
// var text = arabic;
|
||||
// var metrics = textPaint.FontMetrics;
|
||||
//
|
||||
// container
|
||||
// .Padding(25)
|
||||
// .Canvas((canvas, space) =>
|
||||
// {
|
||||
// canvas.Translate(0, 20);
|
||||
//
|
||||
// var width = MeasureText(text, textPaint);
|
||||
// var widthReal = textPaint.MeasureText(text);
|
||||
// canvas.DrawRect(0, metrics.Descent, width, metrics.Ascent - metrics.Descent, backgroundPaint);
|
||||
//
|
||||
// canvas.DrawShapedText(text, 0, 0, textPaint);
|
||||
//
|
||||
// canvas.Translate(0, 40);
|
||||
// canvas.DrawText(text, 0, 0, textPaint);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void MeasureTest()
|
||||
{
|
||||
using var textPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.Black,
|
||||
Typeface = SKTypeface.CreateDefault(),
|
||||
IsAntialias = true,
|
||||
TextSize = 20
|
||||
};
|
||||
|
||||
var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus.";
|
||||
var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
|
||||
// 012345678901234567890123456789012345678901234567890123456
|
||||
var shaper = new SKShaper(textPaint.Typeface);
|
||||
var result = shaper.Shape(lorem, textPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Previewer;
|
||||
using QuestPDF.ReportSample;
|
||||
using QuestPDF.ReportSample.Layouts;
|
||||
|
||||
//ImagePlaceholder.Solid = true;
|
||||
|
||||
// var model = DataSource.GetReport();
|
||||
// var report = new StandardReport(model);
|
||||
// report.ShowInPreviewer();
|
||||
//
|
||||
// return;
|
||||
|
||||
Document
|
||||
.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header()
|
||||
.Text("Hot Reload!")
|
||||
.SemiBold().FontSize(36).FontColor(Colors.Blue.Darken2);
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(20);
|
||||
|
||||
x.Item().Table(t =>
|
||||
{
|
||||
t.ColumnsDefinition(c =>
|
||||
{
|
||||
c.RelativeColumn();
|
||||
c.RelativeColumn(3);
|
||||
});
|
||||
|
||||
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Visual Studio");
|
||||
t.Cell().Border(1).Padding(5).Text("Start in debug mode with 'Hot Reload on Save' enabled.");
|
||||
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Command line");
|
||||
t.Cell().Border(1).Padding(5).Text("Run 'dotnet watch'.");
|
||||
});
|
||||
|
||||
x.Item().Text("Modify this line and the preview should show your changes instantly.");
|
||||
});
|
||||
|
||||
page.Footer()
|
||||
.AlignCenter()
|
||||
.Text(x =>
|
||||
{
|
||||
x.Span("Page ");
|
||||
x.CurrentPageNumber();
|
||||
});
|
||||
});
|
||||
})
|
||||
.ShowInPreviewer();
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
|
||||
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class CommunicationService
|
||||
{
|
||||
public static CommunicationService Instance { get; } = new ();
|
||||
|
||||
public event Action<ICollection<PreviewPage>>? OnDocumentRefreshed;
|
||||
|
||||
private WebApplication? Application { get; set; }
|
||||
|
||||
private readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private CommunicationService()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Task Start(int port)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging(x => x.ClearProviders());
|
||||
builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null);
|
||||
Application = builder.Build();
|
||||
|
||||
Application.MapGet("ping", HandlePing);
|
||||
Application.MapGet("version", HandleVersion);
|
||||
Application.MapPost("update/preview", HandleUpdatePreview);
|
||||
|
||||
return Application.RunAsync($"http://localhost:{port}/");
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
{
|
||||
await Application.StopAsync();
|
||||
await Application.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task<IResult> HandlePing()
|
||||
{
|
||||
return OnDocumentRefreshed == null
|
||||
? Results.StatusCode(StatusCodes.Status503ServiceUnavailable)
|
||||
: Results.Ok();
|
||||
}
|
||||
|
||||
private async Task<IResult> HandleVersion()
|
||||
{
|
||||
return Results.Json(GetType().Assembly.GetName().Version);
|
||||
}
|
||||
|
||||
private async Task<IResult> HandleUpdatePreview(HttpRequest request)
|
||||
{
|
||||
var command = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
|
||||
|
||||
var pages = command
|
||||
.Pages
|
||||
.Select(page =>
|
||||
{
|
||||
using var stream = request.Form.Files[page.Id].OpenReadStream();
|
||||
var picture = SKPicture.Deserialize(stream);
|
||||
|
||||
return new PreviewPage(picture, page.Width, page.Height);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
Task.Run(() => OnDocumentRefreshed(pages));
|
||||
return Results.Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class Helpers
|
||||
{
|
||||
public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> pages)
|
||||
{
|
||||
using var stream = File.Create(filePath);
|
||||
|
||||
using var document = SKDocument.CreatePdf(stream);
|
||||
|
||||
foreach (var page in pages)
|
||||
{
|
||||
using var canvas = document.BeginPage(page.Width, page.Height);
|
||||
canvas.DrawPicture(page.Picture);
|
||||
document.EndPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Rendering.SceneGraph;
|
||||
using Avalonia.Skia;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer;
|
||||
|
||||
class InteractiveCanvas : ICustomDrawOperation
|
||||
{
|
||||
public Rect Bounds { get; set; }
|
||||
public ICollection<PreviewPage> Pages { get; set; }
|
||||
|
||||
private float Width => (float)Bounds.Width;
|
||||
private float Height => (float)Bounds.Height;
|
||||
|
||||
public float Scale { get; private set; } = 1;
|
||||
public float TranslateX { get; set; }
|
||||
public float TranslateY { get; set; }
|
||||
|
||||
private const float MinScale = 0.1f;
|
||||
private const float MaxScale = 10f;
|
||||
|
||||
private const float PageSpacing = 25f;
|
||||
private const float SafeZone = 25f;
|
||||
|
||||
public float TotalPagesHeight => Pages.Sum(x => x.Height) + (Pages.Count - 1) * PageSpacing;
|
||||
public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
|
||||
public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
|
||||
|
||||
public float MaxTranslateY => TotalHeight - Height / Scale;
|
||||
|
||||
public float ScrollPercentY
|
||||
{
|
||||
get
|
||||
{
|
||||
return TranslateY / MaxTranslateY;
|
||||
}
|
||||
set
|
||||
{
|
||||
TranslateY = value * MaxTranslateY;
|
||||
}
|
||||
}
|
||||
|
||||
public float ScrollViewportSizeY
|
||||
{
|
||||
get
|
||||
{
|
||||
var viewPortSize = Height / Scale / TotalHeight;
|
||||
return Math.Clamp(viewPortSize, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#region transformations
|
||||
|
||||
private void LimitScale()
|
||||
{
|
||||
Scale = Math.Max(Scale, MinScale);
|
||||
Scale = Math.Min(Scale, MaxScale);
|
||||
}
|
||||
|
||||
private void LimitTranslate()
|
||||
{
|
||||
if (TotalPagesHeight > Height / Scale)
|
||||
{
|
||||
TranslateY = Math.Min(TranslateY, MaxTranslateY);
|
||||
TranslateY = Math.Max(TranslateY, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateY = (TotalPagesHeight - Height / Scale) / 2;
|
||||
}
|
||||
|
||||
if (Width / Scale < MaxWidth)
|
||||
{
|
||||
var maxTranslateX = (Width / 2 - SafeZone) / Scale - MaxWidth / 2;
|
||||
|
||||
TranslateX = Math.Min(TranslateX, -maxTranslateX);
|
||||
TranslateX = Math.Max(TranslateX, maxTranslateX);
|
||||
}
|
||||
else
|
||||
{
|
||||
TranslateX = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void TranslateWithCurrentScale(float x, float y)
|
||||
{
|
||||
TranslateX += x / Scale;
|
||||
TranslateY += y / Scale;
|
||||
|
||||
LimitTranslate();
|
||||
}
|
||||
|
||||
public void ZoomToPoint(float x, float y, float factor)
|
||||
{
|
||||
var oldScale = Scale;
|
||||
Scale *= factor;
|
||||
|
||||
LimitScale();
|
||||
|
||||
TranslateX -= x / Scale - x / oldScale;
|
||||
TranslateY -= y / Scale - y / oldScale;
|
||||
|
||||
LimitTranslate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region rendering
|
||||
|
||||
public void Render(IDrawingContextImpl context)
|
||||
{
|
||||
if (Pages.Count <= 0)
|
||||
return;
|
||||
|
||||
LimitScale();
|
||||
LimitTranslate();
|
||||
|
||||
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
|
||||
|
||||
if (canvas == null)
|
||||
throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
|
||||
|
||||
var originalMatrix = canvas.TotalMatrix;
|
||||
|
||||
canvas.Translate(Width / 2, 0);
|
||||
|
||||
canvas.Scale(Scale);
|
||||
canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
|
||||
|
||||
foreach (var page in Pages)
|
||||
{
|
||||
canvas.Translate(-page.Width / 2f, 0);
|
||||
DrawBlankPage(canvas, page.Width, page.Height);
|
||||
canvas.DrawPicture(page.Picture);
|
||||
canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
|
||||
}
|
||||
|
||||
canvas.SetMatrix(originalMatrix);
|
||||
DrawInnerGradient(canvas);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public bool Equals(ICustomDrawOperation? other) => false;
|
||||
public bool HitTest(Point p) => true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region blank page
|
||||
|
||||
private static SKPaint BlankPagePaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.White
|
||||
};
|
||||
|
||||
private static SKPaint BlankPageShadowPaint = new SKPaint
|
||||
{
|
||||
ImageFilter = SKImageFilter.CreateBlendMode(
|
||||
SKBlendMode.Overlay,
|
||||
SKImageFilter.CreateDropShadowOnly(0, 6, 6, 6, SKColors.Black.WithAlpha(64)),
|
||||
SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32)))
|
||||
};
|
||||
|
||||
private void DrawBlankPage(SKCanvas canvas, float width, float height)
|
||||
{
|
||||
canvas.DrawRect(0, 0, width, height, BlankPageShadowPaint);
|
||||
canvas.DrawRect(0, 0, width, height, BlankPagePaint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region inner viewport gradient
|
||||
|
||||
private const int InnerGradientSize = (int)SafeZone;
|
||||
private static readonly SKColor InnerGradientColor = SKColor.Parse("#666");
|
||||
|
||||
private void DrawInnerGradient(SKCanvas canvas)
|
||||
{
|
||||
// gamma correction
|
||||
var colors = Enumerable
|
||||
.Range(0, InnerGradientSize)
|
||||
.Select(x => 1f - x / (float) InnerGradientSize)
|
||||
.Select(x => Math.Pow(x, 2f))
|
||||
.Select(x => (byte)(x * 255))
|
||||
.Select(x => InnerGradientColor.WithAlpha(x))
|
||||
.ToArray();
|
||||
|
||||
using var fogPaint = new SKPaint
|
||||
{
|
||||
Shader = SKShader.CreateLinearGradient(
|
||||
new SKPoint(0, 0),
|
||||
new SKPoint(0, InnerGradientSize),
|
||||
colors,
|
||||
SKShaderTileMode.Clamp)
|
||||
};
|
||||
|
||||
canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Previewer
|
||||
{
|
||||
record PreviewPage(SKPicture Picture, float Width, float Height);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.6.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 |
@@ -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
|
||||
```
|
||||
@@ -30,12 +30,12 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
{
|
||||
column.Item().ShowOnce().Padding(5).AlignMiddle().Row(row =>
|
||||
{
|
||||
row.RelativeItem(2).AlignMiddle().Text("PRIMARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
|
||||
row.RelativeItem(2).AlignMiddle().Text("PRIMARY HEADER").FontColor(Colors.Grey.Darken3).FontSize(30).Bold();
|
||||
row.RelativeItem(1).AlignRight().MinimalBox().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30);
|
||||
});
|
||||
column.Item().SkipOnce().Padding(5).Row(row =>
|
||||
{
|
||||
row.RelativeItem(2).Text("SECONDARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
|
||||
row.RelativeItem(2).Text("SECONDARY HEADER").FontColor(Colors.Grey.Darken3).FontSize(30).Bold();
|
||||
row.RelativeItem(1).AlignRight().MinimalBox().Background(Colors.Blue.Lighten4).Padding(15);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -23,7 +23,8 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
decoration
|
||||
.Before()
|
||||
.PaddingBottom(5)
|
||||
.Text(Model.Title, Typography.Headline);
|
||||
.Text(Model.Title)
|
||||
.Style(Typography.Headline);
|
||||
|
||||
decoration.Content().Border(0.75f).BorderColor(Colors.Grey.Medium).Column(column =>
|
||||
{
|
||||
@@ -69,7 +70,7 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
{
|
||||
if (model.PhotoCount == 0)
|
||||
{
|
||||
container.Text("No photos", Typography.Normal);
|
||||
container.Text("No photos").Style(Typography.Normal);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
|
||||
page.Footer().AlignCenter().Text(text =>
|
||||
{
|
||||
text.CurrentPageNumber(format: x => x?.FormatAsRomanNumeral() ?? "-----");
|
||||
text.CurrentPageNumber().Format(x => x?.FormatAsRomanNumeral() ?? "-----");
|
||||
text.Span(" / ");
|
||||
text.TotalPages(format: x => x?.FormatAsRomanNumeral() ?? "-----");
|
||||
text.TotalPages().Format(x => x?.FormatAsRomanNumeral() ?? "-----");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -54,8 +54,8 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
{
|
||||
row.Spacing(50);
|
||||
|
||||
row.RelativeItem().PaddingTop(-10).Text(Model.Title, Typography.Title);
|
||||
row.ConstantItem(90).ExternalLink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
|
||||
row.RelativeItem().PaddingTop(-10).Text(Model.Title).Style(Typography.Title);
|
||||
row.ConstantItem(90).Hyperlink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
|
||||
});
|
||||
|
||||
column.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal();
|
||||
@@ -69,7 +69,7 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
{
|
||||
grid.Item().Text(text =>
|
||||
{
|
||||
text.Span($"{field.Label}: ", TextStyle.Default.SemiBold());
|
||||
text.Span($"{field.Label}: ").SemiBold();
|
||||
text.Span(field.Value);
|
||||
});
|
||||
}
|
||||
@@ -88,14 +88,14 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
column.Item().PageBreak();
|
||||
|
||||
foreach (var section in Model.Sections)
|
||||
column.Item().Location(section.Title).Component(new SectionTemplate(section));
|
||||
column.Item().Section(section.Title).Component(new SectionTemplate(section));
|
||||
|
||||
column.Item().PageBreak();
|
||||
column.Item().Location("Photos");
|
||||
column.Item().Section("Photos");
|
||||
|
||||
foreach (var photo in Model.Photos)
|
||||
column.Item().Component(new PhotoTemplate(photo));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Drawing;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.ReportSample.Layouts
|
||||
{
|
||||
@@ -23,7 +24,8 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
decoration
|
||||
.Before()
|
||||
.PaddingBottom(5)
|
||||
.Text("Table of contents", Typography.Headline);
|
||||
.Text("Table of contents")
|
||||
.Style(Typography.Headline);
|
||||
|
||||
decoration.Content().Column(column =>
|
||||
{
|
||||
@@ -40,24 +42,37 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
private void DrawLink(IContainer container, int number, string locationName)
|
||||
{
|
||||
container
|
||||
.InternalLink(locationName)
|
||||
.SectionLink(locationName)
|
||||
.Row(row =>
|
||||
{
|
||||
row.ConstantItem(25).Text($"{number}.");
|
||||
row.RelativeItem().Text(locationName);
|
||||
row.ConstantItem(150).AlignRight().Text(text =>
|
||||
row.ConstantItem(20).Text($"{number}.");
|
||||
row.AutoItem().Text(locationName);
|
||||
|
||||
row.RelativeItem().PaddingHorizontal(2).AlignBottom().TranslateY(-3).Height(1).Canvas((canvas, space) =>
|
||||
{
|
||||
// best to statically cache
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
StrokeWidth = space.Height,
|
||||
PathEffect = SKPathEffect.CreateDash(new float[] { 1, 3 }, 0)
|
||||
};
|
||||
|
||||
canvas.DrawLine(0, 0, space.Width, 0, paint);
|
||||
});
|
||||
|
||||
row.AutoItem().Text(text =>
|
||||
{
|
||||
text.BeginPageNumberOfSection(locationName);
|
||||
text.Span(" - ");
|
||||
text.EndPageNumberOfSection(locationName);
|
||||
|
||||
var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium);
|
||||
var lengthStyle = TextStyle.Default.FontColor(Colors.Grey.Medium);
|
||||
|
||||
text.Span(" (", lengthStyle);
|
||||
text.TotalPagesWithinSection(locationName, lengthStyle, x => x == 1 ? "1 page long" : $"{x} pages long");
|
||||
text.Span(")", lengthStyle);
|
||||
text.Span(" (").Style(lengthStyle);
|
||||
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => x == 1 ? "1 page long" : $"{x} pages long");
|
||||
text.Span(")").Style(lengthStyle);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>8</LangVersion>
|
||||
<RootNamespace>QuestPDF.ReportSample</RootNamespace>
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
@@ -6,8 +6,8 @@ namespace QuestPDF.ReportSample
|
||||
{
|
||||
public static class Typography
|
||||
{
|
||||
public static TextStyle Title => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Darken3).Size(26).Black();
|
||||
public static TextStyle Headline => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Medium).Size(16).SemiBold();
|
||||
public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Verdana).Color(Colors.Black).Size(10).LineHeight(1.2f);
|
||||
public static TextStyle Title => TextStyle.Default.FontFamily(Fonts.Calibri).FontColor(Colors.Blue.Darken3).FontSize(26).Black();
|
||||
public static TextStyle Headline => TextStyle.Default.FontFamily(Fonts.Calibri).FontColor(Colors.Blue.Medium).FontSize(16).SemiBold();
|
||||
public static TextStyle Normal => TextStyle.Default.FontFamily(Fonts.Verdana).FontColor(Colors.Black).FontSize(10).LineHeight(1.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -10,7 +11,6 @@ namespace QuestPDF.UnitTests.TestEngine
|
||||
public Action<float> RotateFunc { get; set; }
|
||||
public Action<float, float> ScaleFunc { get; set; }
|
||||
public Action<SKImage, Position, Size> DrawImageFunc { get; set; }
|
||||
public Action<string, Position, TextStyle> DrawTextFunc { get; set; }
|
||||
public Action<Position, Size, string> DrawRectFunc { get; set; }
|
||||
|
||||
public void Translate(Position vector) => TranslateFunc(vector);
|
||||
@@ -18,7 +18,7 @@ namespace QuestPDF.UnitTests.TestEngine
|
||||
public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
|
||||
|
||||
public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
|
||||
public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
|
||||
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
|
||||
public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
|
||||
|
||||
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.UnitTests.TestEngine.Operations;
|
||||
using SkiaSharp;
|
||||
@@ -15,7 +16,7 @@ namespace QuestPDF.UnitTests.TestEngine
|
||||
public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));
|
||||
|
||||
public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
|
||||
public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
|
||||
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
|
||||
public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
|
||||
|
||||
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
||||
|
||||
@@ -82,19 +82,6 @@ namespace QuestPDF.UnitTests.TestEngine
|
||||
|
||||
Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
|
||||
},
|
||||
DrawTextFunc = (text, position, style) =>
|
||||
{
|
||||
var expected = GetExpected<CanvasDrawTextOperation>();
|
||||
|
||||
Assert.AreEqual(expected.Text, text);
|
||||
|
||||
Assert.AreEqual(expected.Position.X, position.X, "Draw text: X");
|
||||
Assert.AreEqual(expected.Position.Y, position.Y, "Draw text: Y");
|
||||
|
||||
Assert.AreEqual(expected.Style.Color, style.Color, "Draw text: color");
|
||||
Assert.AreEqual(expected.Style.FontType, style.FontType, "Draw text: font");
|
||||
Assert.AreEqual(expected.Style.Size, style.Size, "Draw text: size");
|
||||
},
|
||||
DrawImageFunc = (image, position, size) =>
|
||||
{
|
||||
var expected = GetExpected<CanvasDrawImageOperation>();
|
||||
@@ -201,11 +188,6 @@ namespace QuestPDF.UnitTests.TestEngine
|
||||
return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
|
||||
}
|
||||
|
||||
public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style)
|
||||
{
|
||||
return AddOperation(new CanvasDrawTextOperation(text, position, style));
|
||||
}
|
||||
|
||||
public TestPlan ExpectCanvasDrawImage(Position position, Size size)
|
||||
{
|
||||
return AddOperation(new CanvasDrawImageOperation(position, size));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
[assembly: InternalsVisibleTo("QuestPDF.UnitTests")]
|
||||
[assembly: InternalsVisibleTo("QuestPDF.Examples")]
|
||||
[assembly: InternalsVisibleTo("QuestPDF.ReportSample")]
|
||||
@@ -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();
|
||||
@@ -117,11 +135,10 @@ namespace QuestPDF.Drawing
|
||||
$"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
|
||||
$"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
|
||||
$"Please analyze your documents structure to detect this element and fix its size constraints.";
|
||||
|
||||
throw new DocumentLayoutException(message)
|
||||
{
|
||||
ElementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode."
|
||||
};
|
||||
|
||||
var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode.";
|
||||
|
||||
throw new DocumentLayoutException(message, elementTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +163,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 +185,9 @@ namespace QuestPDF.Drawing
|
||||
return;
|
||||
}
|
||||
|
||||
if (content is DynamicHost dynamicHost)
|
||||
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
var targetTextStyle = documentDefaultTextStyle;
|
||||
|
||||
if (content is DefaultTextStyle defaultTextStyleElement)
|
||||
|
||||
@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
|
||||
{
|
||||
public class DocumentComposeException : Exception
|
||||
{
|
||||
public DocumentComposeException()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DocumentComposeException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DocumentComposeException(string message, Exception inner) : base(message, inner)
|
||||
internal DocumentComposeException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
|
||||
{
|
||||
public class DocumentDrawingException : Exception
|
||||
{
|
||||
public DocumentDrawingException()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DocumentDrawingException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DocumentDrawingException(string message, Exception inner) : base(message, inner)
|
||||
internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -4,21 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
|
||||
{
|
||||
public class DocumentLayoutException : Exception
|
||||
{
|
||||
public string ElementTrace { get; set; }
|
||||
|
||||
public DocumentLayoutException()
|
||||
{
|
||||
|
||||
}
|
||||
public string? ElementTrace { get; }
|
||||
|
||||
public DocumentLayoutException(string message) : base(message)
|
||||
internal DocumentLayoutException(string message, string? elementTrace = null) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DocumentLayoutException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
|
||||
ElementTrace = elementTrace;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace QuestPDF.Drawing.Exceptions
|
||||
{
|
||||
public class InitializationException : Exception
|
||||
{
|
||||
internal InitializationException(string documentType, Exception innerException) : base(CreateMessage(documentType), innerException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private static string CreateMessage(string documentType)
|
||||
{
|
||||
return $"Cannot create the {documentType} document using the SkiaSharp library. " +
|
||||
$"This exception usually means that, on your operating system where you run the application, SkiaSharp requires installing additional dependencies. " +
|
||||
$"Such dependencies are available as additional nuget packages, for example SkiaSharp.NativeAssets.Linux. " +
|
||||
$"Please refer to the SkiaSharp documentation for more details.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using HarfBuzzSharp;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.HarfBuzz;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
@@ -11,8 +14,11 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
||||
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
|
||||
private static ConcurrentDictionary<object, SKPaint> Paints = new();
|
||||
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
|
||||
private static ConcurrentDictionary<object, SKPaint> FontPaints = new();
|
||||
private static ConcurrentDictionary<string, SKPaint> ColorPaints = new();
|
||||
private static ConcurrentDictionary<object, Font> ShaperFonts = new();
|
||||
private static ConcurrentDictionary<object, SKFont> Fonts = new();
|
||||
private static ConcurrentDictionary<object, TextShaper> TextShapers = new();
|
||||
|
||||
private static void RegisterFontType(SKData fontData, string? customName = null)
|
||||
{
|
||||
@@ -30,7 +36,7 @@ namespace QuestPDF.Drawing
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Since version 2022.3, the FontManager class offers better font type matching support. Please use the RegisterFontType(Stream stream) overload.")]
|
||||
[Obsolete("Since version 2022.3, the FontManager class offers better font type matching support. Please use the RegisterFont(Stream stream) method.")]
|
||||
public static void RegisterFontType(string fontName, Stream stream)
|
||||
{
|
||||
using var fontData = SKData.Create(stream);
|
||||
@@ -38,7 +44,7 @@ namespace QuestPDF.Drawing
|
||||
RegisterFontType(fontData, customName: fontName);
|
||||
}
|
||||
|
||||
public static void RegisterFontType(Stream stream)
|
||||
public static void RegisterFont(Stream stream)
|
||||
{
|
||||
using var fontData = SKData.Create(stream);
|
||||
RegisterFontType(fontData);
|
||||
@@ -46,20 +52,21 @@ namespace QuestPDF.Drawing
|
||||
|
||||
internal static SKPaint ColorToPaint(this string color)
|
||||
{
|
||||
return ColorPaint.GetOrAdd(color, Convert);
|
||||
return ColorPaints.GetOrAdd(color, Convert);
|
||||
|
||||
static SKPaint Convert(string color)
|
||||
{
|
||||
return new SKPaint
|
||||
{
|
||||
Color = SKColor.Parse(color)
|
||||
Color = SKColor.Parse(color),
|
||||
IsAntialias = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal static SKPaint ToPaint(this TextStyle style)
|
||||
{
|
||||
return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
|
||||
return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style));
|
||||
|
||||
static SKPaint Convert(TextStyle style)
|
||||
{
|
||||
@@ -67,36 +74,91 @@ 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);
|
||||
|
||||
if (StyleSets.TryGetValue(style.FontType, out var fontStyleSet))
|
||||
if (StyleSets.TryGetValue(style.FontFamily, out var fontStyleSet))
|
||||
return fontStyleSet.Match(fontStyle);
|
||||
|
||||
var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontType, fontStyle);
|
||||
var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontFamily, fontStyle);
|
||||
|
||||
if (fontFromDefaultSource != null)
|
||||
return fontFromDefaultSource;
|
||||
|
||||
throw new ArgumentException(
|
||||
$"The typeface '{style.FontType}' could not be found. " +
|
||||
$"The typeface '{style.FontFamily}' could not be found. " +
|
||||
$"Please consider the following options: " +
|
||||
$"1) install the font on your operating system or execution environment. " +
|
||||
$"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method.");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
internal static Font ToShaperFont(this TextStyle style)
|
||||
{
|
||||
return ShaperFonts.GetOrAdd(style.PaintKey, _ =>
|
||||
{
|
||||
var typeface = style.ToPaint().Typeface;
|
||||
|
||||
using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob();
|
||||
|
||||
using var face = new Face(harfBuzzBlob, ttcIndex)
|
||||
{
|
||||
Index = ttcIndex,
|
||||
UnitsPerEm = typeface.UnitsPerEm,
|
||||
GlyphCount = typeface.GlyphCount
|
||||
};
|
||||
|
||||
var font = new Font(face);
|
||||
font.SetScale(TextShaper.FontShapingScale, TextShaper.FontShapingScale);
|
||||
font.SetFunctionsOpenType();
|
||||
|
||||
return font;
|
||||
});
|
||||
}
|
||||
|
||||
internal static TextShaper ToTextShaper(this TextStyle style)
|
||||
{
|
||||
return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style));
|
||||
}
|
||||
|
||||
internal static SKFont FoFont(this TextStyle style)
|
||||
{
|
||||
return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ namespace QuestPDF.Drawing
|
||||
|
||||
}
|
||||
|
||||
public void DrawText(string text, Position position, TextStyle style)
|
||||
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Helpers;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -7,11 +9,23 @@ namespace QuestPDF.Drawing
|
||||
internal class PdfCanvas : SkiaDocumentCanvasBase
|
||||
{
|
||||
public PdfCanvas(Stream stream, DocumentMetadata documentMetadata)
|
||||
: base(SKDocument.CreatePdf(stream, MapMetadata(documentMetadata)))
|
||||
: base(CreatePdf(stream, documentMetadata))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata));
|
||||
}
|
||||
catch (TypeInitializationException exception)
|
||||
{
|
||||
throw new InitializationException("PDF", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata)
|
||||
{
|
||||
return new SKDocumentPdfMetadata
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.HarfBuzz;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
@@ -27,9 +28,9 @@ namespace QuestPDF.Drawing
|
||||
Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
|
||||
}
|
||||
|
||||
public void DrawText(string text, Position vector, TextStyle style)
|
||||
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
|
||||
{
|
||||
Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
|
||||
Canvas.DrawText(skTextBlob, position.X, position.Y, style.ToPaint());
|
||||
}
|
||||
|
||||
public void DrawImage(SKImage image, Position vector, Size size)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using HarfBuzzSharp;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
using Buffer = HarfBuzzSharp.Buffer;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
internal class TextShaper
|
||||
{
|
||||
public const int FontShapingScale = 512;
|
||||
|
||||
private Font Font { get; }
|
||||
private SKPaint Paint { get; }
|
||||
|
||||
public TextShaper(TextStyle style)
|
||||
{
|
||||
Font = style.ToShaperFont();
|
||||
Paint = style.ToPaint();
|
||||
}
|
||||
|
||||
public TextShapingResult Shape(string text)
|
||||
{
|
||||
var buffer = new Buffer();
|
||||
|
||||
PopulateBufferWithText(buffer, text);
|
||||
buffer.GuessSegmentProperties();
|
||||
|
||||
Font.Shape(buffer);
|
||||
|
||||
var length = buffer.Length;
|
||||
var glyphInfos = buffer.GlyphInfos;
|
||||
var glyphPositions = buffer.GlyphPositions;
|
||||
|
||||
var scaleY = Paint.TextSize / FontShapingScale;
|
||||
var scaleX = scaleY * Paint.TextScaleX;
|
||||
|
||||
var xOffset = 0f;
|
||||
var yOffset = 0f;
|
||||
|
||||
var glyphs = new ShapedGlyph[length];
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
glyphs[i] = new ShapedGlyph
|
||||
{
|
||||
Codepoint = (ushort)glyphInfos[i].Codepoint,
|
||||
Position = new SKPoint(xOffset + glyphPositions[i].XOffset * scaleX, yOffset - glyphPositions[i].YOffset * scaleY),
|
||||
Width = glyphPositions[i].XAdvance * scaleX
|
||||
};
|
||||
|
||||
xOffset += glyphPositions[i].XAdvance * scaleX;
|
||||
yOffset += glyphPositions[i].YAdvance * scaleY;
|
||||
}
|
||||
|
||||
return new TextShapingResult(glyphs);
|
||||
}
|
||||
|
||||
void PopulateBufferWithText(Buffer buffer, string text)
|
||||
{
|
||||
var encoding = Paint.TextEncoding;
|
||||
|
||||
if (encoding == SKTextEncoding.Utf8)
|
||||
buffer.AddUtf8(text);
|
||||
|
||||
else if (encoding == SKTextEncoding.Utf16)
|
||||
buffer.AddUtf16(text);
|
||||
|
||||
else if (encoding == SKTextEncoding.Utf32)
|
||||
buffer.AddUtf32(text);
|
||||
|
||||
else
|
||||
throw new NotSupportedException("TextEncoding of type GlyphId is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
internal struct ShapedGlyph
|
||||
{
|
||||
public ushort Codepoint;
|
||||
public SKPoint Position;
|
||||
public float Width;
|
||||
}
|
||||
|
||||
internal struct DrawTextCommand
|
||||
{
|
||||
public SKTextBlob SkTextBlob;
|
||||
public float TextOffsetX;
|
||||
}
|
||||
|
||||
internal class TextShapingResult
|
||||
{
|
||||
public ShapedGlyph[] Glyphs { get; }
|
||||
|
||||
public TextShapingResult(ShapedGlyph[] glyphs)
|
||||
{
|
||||
Glyphs = glyphs;
|
||||
}
|
||||
|
||||
public int BreakText(int startIndex, float maxWidth)
|
||||
{
|
||||
var index = startIndex;
|
||||
maxWidth += Glyphs[startIndex].Position.X;
|
||||
|
||||
while (index < Glyphs.Length)
|
||||
{
|
||||
var glyph = Glyphs[index];
|
||||
|
||||
if (glyph.Position.X + glyph.Width > maxWidth + Size.Epsilon)
|
||||
break;
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return index - 1;
|
||||
}
|
||||
|
||||
public float MeasureWidth(int startIndex, int endIndex)
|
||||
{
|
||||
if (Glyphs.Length == 0)
|
||||
return 0;
|
||||
|
||||
var start = Glyphs[startIndex];
|
||||
var end = Glyphs[endIndex];
|
||||
|
||||
return end.Position.X - start.Position.X + end.Width;
|
||||
}
|
||||
|
||||
public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle)
|
||||
{
|
||||
if (Glyphs.Length == 0)
|
||||
return null;
|
||||
|
||||
using var skTextBlobBuilder = new SKTextBlobBuilder();
|
||||
|
||||
var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(textStyle.FoFont(), endIndex - startIndex + 1);
|
||||
var glyphSpan = positionedRunBuffer.GetGlyphSpan();
|
||||
var positionSpan = positionedRunBuffer.GetPositionSpan();
|
||||
|
||||
for (var sourceIndex = startIndex; sourceIndex <= endIndex; sourceIndex++)
|
||||
{
|
||||
var runIndex = sourceIndex - startIndex;
|
||||
|
||||
glyphSpan[runIndex] = Glyphs[sourceIndex].Codepoint;
|
||||
positionSpan[runIndex] = Glyphs[sourceIndex].Position;
|
||||
}
|
||||
|
||||
return new DrawTextCommand
|
||||
{
|
||||
SkTextBlob = skTextBlobBuilder.Build(),
|
||||
TextOffsetX = -Glyphs[startIndex].Position.X
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Helpers;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -7,9 +9,21 @@ namespace QuestPDF.Drawing
|
||||
internal class XpsCanvas : SkiaDocumentCanvasBase
|
||||
{
|
||||
public XpsCanvas(Stream stream, DocumentMetadata documentMetadata)
|
||||
: base(SKDocument.CreateXps(stream, documentMetadata.RasterDpi))
|
||||
: base(CreateXps(stream, documentMetadata))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private static SKDocument CreateXps(Stream stream, DocumentMetadata documentMetadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SKDocument.CreateXps(stream, documentMetadata.RasterDpi);
|
||||
}
|
||||
catch (TypeInitializationException exception)
|
||||
{
|
||||
throw new InitializationException("XPS", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,10 @@ namespace QuestPDF.Elements
|
||||
.MinimalBox()
|
||||
.Background(Colors.White)
|
||||
.Padding(2)
|
||||
.Text(Text, TextStyle.Default.Color(Color).FontType(Fonts.Consolas).Size(8));
|
||||
.Text(Text)
|
||||
.FontColor(Color)
|
||||
.FontFamily(Fonts.Consolas)
|
||||
.FontSize(8);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
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,
|
||||
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
|
||||
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 int TotalPages { 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; }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
+46
-29
@@ -20,6 +20,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
public string BackgroundColor { get; set; } = Colors.Transparent;
|
||||
|
||||
public Element Background { get; set; } = Empty.Instance;
|
||||
public Element Foreground { get; set; } = Empty.Instance;
|
||||
|
||||
public Element Header { get; set; } = Empty.Instance;
|
||||
public Element Content { get; set; } = Empty.Instance;
|
||||
public Element Footer { get; set; } = Empty.Instance;
|
||||
@@ -27,39 +30,53 @@ namespace QuestPDF.Elements
|
||||
public void Compose(IContainer container)
|
||||
{
|
||||
container
|
||||
.MinWidth(MinSize.Width)
|
||||
.MinHeight(MinSize.Height)
|
||||
|
||||
.MaxWidth(MaxSize.Width)
|
||||
.MaxHeight(MaxSize.Height)
|
||||
|
||||
.Background(BackgroundColor)
|
||||
|
||||
.PaddingLeft(MarginLeft)
|
||||
.PaddingRight(MarginRight)
|
||||
.PaddingTop(MarginTop)
|
||||
.PaddingBottom(MarginBottom)
|
||||
|
||||
.DefaultTextStyle(DefaultTextStyle)
|
||||
|
||||
.Decoration(decoration =>
|
||||
.Layers(layers =>
|
||||
{
|
||||
decoration
|
||||
.Before()
|
||||
.DebugPointer("Page header")
|
||||
.Element(Header);
|
||||
layers
|
||||
.Layer()
|
||||
.DebugPointer("Page background layer")
|
||||
.Element(Background);
|
||||
|
||||
decoration
|
||||
.Content()
|
||||
.Element(x => IsClose(MinSize.Width, MaxSize.Width) ? x.ExtendHorizontal() : x)
|
||||
.Element(x => IsClose(MinSize.Height, MaxSize.Height) ? x.ExtendVertical() : x)
|
||||
.DebugPointer("Page content")
|
||||
.Element(Content);
|
||||
layers
|
||||
.PrimaryLayer()
|
||||
.MinWidth(MinSize.Width)
|
||||
.MinHeight(MinSize.Height)
|
||||
|
||||
.MaxWidth(MaxSize.Width)
|
||||
.MaxHeight(MaxSize.Height)
|
||||
|
||||
.PaddingLeft(MarginLeft)
|
||||
.PaddingRight(MarginRight)
|
||||
.PaddingTop(MarginTop)
|
||||
.PaddingBottom(MarginBottom)
|
||||
|
||||
.DefaultTextStyle(DefaultTextStyle)
|
||||
|
||||
.Decoration(decoration =>
|
||||
{
|
||||
decoration
|
||||
.Before()
|
||||
.DebugPointer("Page header")
|
||||
.Element(Header);
|
||||
|
||||
decoration
|
||||
.Content()
|
||||
.Element(x => IsClose(MinSize.Width, MaxSize.Width) ? x.ExtendHorizontal() : x)
|
||||
.Element(x => IsClose(MinSize.Height, MaxSize.Height) ? x.ExtendVertical() : x)
|
||||
.DebugPointer("Page content")
|
||||
.Element(Content);
|
||||
|
||||
decoration
|
||||
.After()
|
||||
.DebugPointer("Page footer")
|
||||
.Element(Footer);
|
||||
});
|
||||
|
||||
decoration
|
||||
.After()
|
||||
.DebugPointer("Page footer")
|
||||
.Element(Footer);
|
||||
layers
|
||||
.Layer()
|
||||
.DebugPointer("Page foreground layer")
|
||||
.Element(Foreground);
|
||||
});
|
||||
|
||||
bool IsClose(float x, float y)
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace QuestPDF.Elements
|
||||
if (string.IsNullOrWhiteSpace(Text))
|
||||
x.MaxHeight(32).Image(ImageData, ImageScaling.FitArea);
|
||||
else
|
||||
x.Text(Text, TextStyle.Default.Size(14));
|
||||
x.Text(Text).FontSize(14);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,13 +102,14 @@ namespace QuestPDF.Elements
|
||||
|
||||
private void UpdateItemsWidth(float availableWidth)
|
||||
{
|
||||
HandleItemsWithAutoWidth();
|
||||
foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto))
|
||||
rowItem.Size = rowItem.Measure(Size.Max).Width;
|
||||
|
||||
var constantWidth = Items.Where(x => x.Type == RowItemType.Constant).Sum(x => x.Size);
|
||||
var constantWidth = Items.Where(x => x.Type != RowItemType.Relative).Sum(x => x.Size);
|
||||
var relativeWidth = Items.Where(x => x.Type == RowItemType.Relative).Sum(x => x.Size);
|
||||
var spacingWidth = (Items.Count - 1) * Spacing;
|
||||
|
||||
foreach (var item in Items.Where(x => x.Type == RowItemType.Constant))
|
||||
foreach (var item in Items.Where(x => x.Type != RowItemType.Relative))
|
||||
item.Width = item.Size;
|
||||
|
||||
if (relativeWidth <= 0)
|
||||
@@ -119,16 +120,7 @@ namespace QuestPDF.Elements
|
||||
foreach (var item in Items.Where(x => x.Type == RowItemType.Relative))
|
||||
item.Width = item.Size * widthPerRelativeUnit;
|
||||
}
|
||||
|
||||
private void HandleItemsWithAutoWidth()
|
||||
{
|
||||
foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto))
|
||||
{
|
||||
rowItem.Size = rowItem.Measure(Size.Max).Width;
|
||||
rowItem.Type = RowItemType.Constant;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ICollection<RowItemRenderingCommand> PlanLayout(Size availableSpace)
|
||||
{
|
||||
var leftOffset = 0f;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using QuestPDF.Fluent;
|
||||
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
static class TableLayoutValidator
|
||||
static class TableLayoutPlanner
|
||||
{
|
||||
public static void PlanCellPositions(this Table table)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using QuestPDF.Fluent;
|
||||
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
static class TableLayoutPlanner
|
||||
static class TableLayoutValidator
|
||||
{
|
||||
public static void ValidateCellPositions(this Table table)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Calculation
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using QuestPDF.Drawing;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Calculation
|
||||
{
|
||||
|
||||
@@ -8,22 +8,23 @@ namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
public const string PageNumberPlaceholder = "123";
|
||||
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
|
||||
protected override bool EnableTextCache => false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockSectionlLink : TextBlockSpan
|
||||
internal class TextBlockSectionLink : TextBlockSpan
|
||||
{
|
||||
public string SectionName { get; set; }
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.HarfBuzz;
|
||||
using Size = QuestPDF.Infrastructure.Size;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Items
|
||||
@@ -9,37 +14,44 @@ namespace QuestPDF.Elements.Text.Items
|
||||
internal class TextBlockSpan : ITextBlockItem
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public TextStyle Style { get; set; } = new TextStyle();
|
||||
public TextStyle Style { get; set; } = new();
|
||||
public TextShapingResult? TextShapingResult { get; set; }
|
||||
|
||||
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache =
|
||||
new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>();
|
||||
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
|
||||
protected virtual bool EnableTextCache => true;
|
||||
|
||||
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||
{
|
||||
var cacheKey = (request.StartIndex, request.AvailableWidth);
|
||||
|
||||
|
||||
if (!MeasureCache.ContainsKey(cacheKey))
|
||||
MeasureCache[cacheKey] = MeasureWithoutCache(request);
|
||||
|
||||
return MeasureCache[cacheKey];
|
||||
}
|
||||
|
||||
|
||||
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
|
||||
{
|
||||
const char space = ' ';
|
||||
if (!EnableTextCache)
|
||||
TextShapingResult = null;
|
||||
|
||||
TextShapingResult ??= Style.ToTextShaper().Shape(Text);
|
||||
|
||||
var paint = Style.ToPaint();
|
||||
var fontMetrics = Style.ToFontMetrics();
|
||||
var spaceCodepoint = paint.ToFont().Typeface.GetGlyphs(" ")[0];
|
||||
|
||||
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 < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint)
|
||||
startIndex++;
|
||||
}
|
||||
|
||||
if (Text.Length == 0)
|
||||
if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
|
||||
{
|
||||
return new TextMeasurementResult
|
||||
{
|
||||
@@ -52,42 +64,19 @@ namespace QuestPDF.Elements.Text.Items
|
||||
}
|
||||
|
||||
// start breaking text from requested position
|
||||
var text = Text.Substring(startIndex);
|
||||
|
||||
var textLength = (int)paint.BreakText(text, request.AvailableWidth);
|
||||
var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
|
||||
|
||||
if (textLength <= 0)
|
||||
if (endIndex < 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 wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
|
||||
|
||||
if (lastSpaceIndex <= 0)
|
||||
{
|
||||
if (!request.IsFirstLineElement)
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
textLength = lastSpaceIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
text = text.Substring(0, textLength);
|
||||
|
||||
var endIndex = startIndex + textLength;
|
||||
var nextIndex = endIndex;
|
||||
|
||||
while (nextIndex + 1 < Text.Length && Text[nextIndex] == space)
|
||||
nextIndex++;
|
||||
if (wrappedText == null)
|
||||
return null;
|
||||
|
||||
// measure final text
|
||||
var width = paint.MeasureText(text);
|
||||
var width = TextShapingResult.MeasureWidth(startIndex, wrappedText.Value.endIndex);
|
||||
|
||||
return new TextMeasurementResult
|
||||
{
|
||||
@@ -99,32 +88,100 @@ namespace QuestPDF.Elements.Text.Items
|
||||
LineHeight = Style.LineHeight ?? 1,
|
||||
|
||||
StartIndex = startIndex,
|
||||
EndIndex = endIndex,
|
||||
NextIndex = nextIndex,
|
||||
TotalIndex = Text.Length
|
||||
EndIndex = wrappedText.Value.endIndex,
|
||||
NextIndex = wrappedText.Value.nextIndex,
|
||||
TotalIndex = TextShapingResult.Glyphs.Length - 1
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: consider introducing text wrapping abstraction (basic, english-like, asian-like)
|
||||
private (int endIndex, int nextIndex)? WrapText(int startIndex, int endIndex, bool isFirstElementInLine)
|
||||
{
|
||||
var spaceCodepoint = Style.ToPaint().ToFont().Typeface.GetGlyphs(" ")[0];
|
||||
|
||||
// textLength - length of the part of the text that fits in available width (creating a line)
|
||||
|
||||
// entire text fits, no need to wrap
|
||||
if (endIndex == TextShapingResult.Glyphs.Length - 1)
|
||||
return (endIndex, endIndex);
|
||||
|
||||
// breaking anywhere
|
||||
if (Style.WrapAnywhere ?? false)
|
||||
return (endIndex, endIndex + 1);
|
||||
|
||||
// current line ends at word, next character is space, perfect place to wrap
|
||||
if (TextShapingResult.Glyphs[endIndex].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex + 1].Codepoint == spaceCodepoint)
|
||||
return (endIndex, endIndex + 2);
|
||||
|
||||
// find last space within the available text to wrap
|
||||
var lastSpaceIndex = endIndex;
|
||||
|
||||
while (lastSpaceIndex >= startIndex)
|
||||
{
|
||||
if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint)
|
||||
break;
|
||||
|
||||
lastSpaceIndex--;
|
||||
}
|
||||
|
||||
// text contains space that can be used to wrap
|
||||
if (lastSpaceIndex >= startIndex)
|
||||
return (lastSpaceIndex - 1, 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 ? (endIndex, endIndex + 1) : null;
|
||||
}
|
||||
|
||||
public virtual void Draw(TextDrawingRequest request)
|
||||
{
|
||||
var fontMetrics = Style.ToFontMetrics();
|
||||
|
||||
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
|
||||
var glyphOffsetY = GetGlyphOffset();
|
||||
|
||||
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);
|
||||
var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex, Style);
|
||||
|
||||
if (Style.BackgroundColor != Colors.Transparent)
|
||||
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
|
||||
|
||||
if (textDrawingCommand.HasValue)
|
||||
request.Canvas.DrawText(textDrawingCommand.Value.SkTextBlob, new Position(textDrawingCommand.Value.TextOffsetX, glyphOffsetY), Style);
|
||||
|
||||
// draw underline
|
||||
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
|
||||
DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1);
|
||||
{
|
||||
var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffsetY;
|
||||
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 + glyphOffsetY, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,15 @@ namespace QuestPDF.Fluent
|
||||
TextStyle = textStyle
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static IContainer DefaultTextStyle(this IContainer element, Func<TextStyle, TextStyle> handler)
|
||||
{
|
||||
return element.Element(new DefaultTextStyle
|
||||
{
|
||||
TextStyle = handler(TextStyle.Default)
|
||||
});
|
||||
}
|
||||
|
||||
public static IContainer StopPaging(this IContainer element)
|
||||
{
|
||||
return element.Element(new StopPaging());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -83,11 +83,36 @@ namespace QuestPDF.Fluent
|
||||
Page.DefaultTextStyle = textStyle;
|
||||
}
|
||||
|
||||
public void Background(string color)
|
||||
public void DefaultTextStyle(Func<TextStyle, TextStyle> handler)
|
||||
{
|
||||
DefaultTextStyle(handler(TextStyle.Default));
|
||||
}
|
||||
|
||||
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();
|
||||
Page.Background = container;
|
||||
return container;
|
||||
}
|
||||
|
||||
public IContainer Foreground()
|
||||
{
|
||||
var container = new Container();
|
||||
Page.Foreground = container;
|
||||
return container;
|
||||
}
|
||||
|
||||
public IContainer Header()
|
||||
{
|
||||
var container = new Container();
|
||||
|
||||
@@ -10,6 +10,34 @@ using static System.String;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
public class TextSpanDescriptor
|
||||
{
|
||||
internal TextStyle TextStyle { get; }
|
||||
|
||||
internal TextSpanDescriptor(TextStyle textStyle)
|
||||
{
|
||||
TextStyle = textStyle;
|
||||
}
|
||||
}
|
||||
|
||||
public delegate string PageNumberFormatter(int? pageNumber);
|
||||
|
||||
public class TextPageNumberDescriptor : TextSpanDescriptor
|
||||
{
|
||||
internal PageNumberFormatter FormatFunction { get; private set; } = x => x?.ToString() ?? string.Empty;
|
||||
|
||||
internal TextPageNumberDescriptor(TextStyle textStyle) : base(textStyle)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
|
||||
{
|
||||
FormatFunction = formatter ?? FormatFunction;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class TextDescriptor
|
||||
{
|
||||
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
|
||||
@@ -22,6 +50,11 @@ namespace QuestPDF.Fluent
|
||||
DefaultStyle = style;
|
||||
}
|
||||
|
||||
public void DefaultTextStyle(Func<TextStyle, TextStyle> style)
|
||||
{
|
||||
DefaultStyle = style(TextStyle.Default);
|
||||
}
|
||||
|
||||
public void AlignLeft()
|
||||
{
|
||||
Alignment = HorizontalAlignment.Left;
|
||||
@@ -50,12 +83,19 @@ namespace QuestPDF.Fluent
|
||||
TextBlocks.Last().Items.Add(item);
|
||||
}
|
||||
|
||||
public void Span(string? text, TextStyle? style = null)
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
|
||||
public void Span(string? text, TextStyle style)
|
||||
{
|
||||
if (IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
style ??= TextStyle.Default;
|
||||
Span(text).Style(style);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -77,102 +117,108 @@ namespace QuestPDF.Fluent
|
||||
})
|
||||
.ToList()
|
||||
.ForEach(TextBlocks.Add);
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public void Line(string? text, TextStyle? style = null)
|
||||
public TextSpanDescriptor Line(string? text)
|
||||
{
|
||||
text ??= string.Empty;
|
||||
Span(text + Environment.NewLine, style);
|
||||
return Span(text + Environment.NewLine);
|
||||
}
|
||||
|
||||
public void EmptyLine()
|
||||
public TextSpanDescriptor EmptyLine()
|
||||
{
|
||||
Span(Environment.NewLine);
|
||||
return Span(Environment.NewLine);
|
||||
}
|
||||
|
||||
private static Func<int?, string> DefaultTextFormat = x => (x ?? 123).ToString();
|
||||
|
||||
private void PageNumber(Func<IPageContext, int?> pageNumber, TextStyle? style, Func<int?, string>? format)
|
||||
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
|
||||
{
|
||||
style ??= TextStyle.Default;
|
||||
format ??= DefaultTextFormat;
|
||||
var style = DefaultStyle.Clone();
|
||||
var descriptor = new TextPageNumberDescriptor(style);
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockPageNumber()
|
||||
AddItemToLastTextBlock(new TextBlockPageNumber
|
||||
{
|
||||
Source = context => format(pageNumber(context)),
|
||||
Source = context => descriptor.FormatFunction(pageNumber(context)),
|
||||
Style = style
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public void CurrentPageNumber(TextStyle? style = null, Func<int?, string>? format = null)
|
||||
public TextPageNumberDescriptor CurrentPageNumber()
|
||||
{
|
||||
PageNumber(x => x.CurrentPage, style, format);
|
||||
return PageNumber(x => x.CurrentPage);
|
||||
}
|
||||
|
||||
public void TotalPages(TextStyle? style = null, Func<int?, string>? format = null)
|
||||
public TextPageNumberDescriptor TotalPages()
|
||||
{
|
||||
PageNumber(x => x.GetLocation(PageContext.DocumentLocation)?.Length, style, format);
|
||||
return PageNumber(x => x.GetLocation(PageContext.DocumentLocation)?.Length);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the BeginPageNumberOfSection method.")]
|
||||
public void PageNumberOfLocation(string locationName, TextStyle? style = null)
|
||||
{
|
||||
BeginPageNumberOfSection(locationName);
|
||||
BeginPageNumberOfSection(locationName).Style(style);
|
||||
}
|
||||
|
||||
public void BeginPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
||||
public TextPageNumberDescriptor BeginPageNumberOfSection(string locationName)
|
||||
{
|
||||
PageNumber(x => x.GetLocation(locationName)?.PageStart, style, format);
|
||||
return PageNumber(x => x.GetLocation(locationName)?.PageStart);
|
||||
}
|
||||
|
||||
public void EndPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
||||
public TextPageNumberDescriptor EndPageNumberOfSection(string locationName)
|
||||
{
|
||||
PageNumber(x => x.GetLocation(locationName)?.PageEnd, style, format);
|
||||
return PageNumber(x => x.GetLocation(locationName)?.PageEnd);
|
||||
}
|
||||
|
||||
public void PageNumberWithinSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
||||
public TextPageNumberDescriptor PageNumberWithinSection(string locationName)
|
||||
{
|
||||
PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd, style, format);
|
||||
return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageStart);
|
||||
}
|
||||
|
||||
public void TotalPagesWithinSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
||||
public TextPageNumberDescriptor TotalPagesWithinSection(string locationName)
|
||||
{
|
||||
PageNumber(x => x.GetLocation(locationName)?.Length, style, format);
|
||||
return PageNumber(x => x.GetLocation(locationName)?.Length);
|
||||
}
|
||||
|
||||
public void SectionLink(string? text, string sectionName, TextStyle? style = null)
|
||||
public TextSpanDescriptor SectionLink(string? text, string sectionName)
|
||||
{
|
||||
if (IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
if (IsNullOrEmpty(sectionName))
|
||||
throw new ArgumentException(nameof(sectionName));
|
||||
throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
|
||||
|
||||
style ??= TextStyle.Default;
|
||||
var style = DefaultStyle.Clone();
|
||||
var descriptor = new TextSpanDescriptor(style);
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockSectionlLink
|
||||
if (IsNullOrEmpty(text))
|
||||
return descriptor;
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockSectionLink
|
||||
{
|
||||
Style = style,
|
||||
Text = text,
|
||||
SectionName = sectionName
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
|
||||
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
|
||||
{
|
||||
SectionLink(text, locationName, style);
|
||||
SectionLink(text, locationName).Style(style);
|
||||
}
|
||||
|
||||
public void Hyperlink(string? text, string url, TextStyle? style = null)
|
||||
public TextSpanDescriptor Hyperlink(string? text, string url)
|
||||
{
|
||||
if (IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
if (IsNullOrEmpty(url))
|
||||
throw new ArgumentException(nameof(url));
|
||||
|
||||
style ??= TextStyle.Default;
|
||||
throw new ArgumentException("Url cannot be null or empty", nameof(url));
|
||||
|
||||
var style = DefaultStyle.Clone();
|
||||
var descriptor = new TextSpanDescriptor(style);
|
||||
|
||||
if (IsNullOrEmpty(text))
|
||||
return descriptor;
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockHyperlink
|
||||
{
|
||||
@@ -180,12 +226,14 @@ namespace QuestPDF.Fluent
|
||||
Text = text,
|
||||
Url = url
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
|
||||
public void ExternalLocation(string? text, string url, TextStyle? style = null)
|
||||
{
|
||||
Hyperlink(text, url, style);
|
||||
Hyperlink(text, url).Style(style);
|
||||
}
|
||||
|
||||
public IContainer Element()
|
||||
@@ -227,9 +275,17 @@ namespace QuestPDF.Fluent
|
||||
descriptor.Compose(element);
|
||||
}
|
||||
|
||||
public static void Text(this IContainer element, object? text, TextStyle? style = null)
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
|
||||
public static void Text(this IContainer element, object? text, TextStyle style)
|
||||
{
|
||||
element.Text(x => x.Span(text?.ToString(), style));
|
||||
element.Text(text).Style(style);
|
||||
}
|
||||
|
||||
public static TextSpanDescriptor Text(this IContainer element, object? text)
|
||||
{
|
||||
var descriptor = (TextSpanDescriptor) null;
|
||||
element.Text(x => descriptor = x.Span(text?.ToString()));
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
public static class TextSpanDescriptorExtensions
|
||||
{
|
||||
public static T Style<T>(this T descriptor, TextStyle style) where T : TextSpanDescriptor
|
||||
{
|
||||
if (style == null)
|
||||
return descriptor;
|
||||
|
||||
descriptor.TextStyle.OverrideStyle(style);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.Color = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T BackgroundColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.BackgroundColor = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T FontFamily<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.FontFamily = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T FontSize<T>(this T descriptor, float value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.Size = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T LineHeight<T>(this T descriptor, float value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.LineHeight = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Italic<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.IsItalic = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Strikethrough<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.HasStrikethrough = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Underline<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.HasUnderline = value;
|
||||
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
|
||||
{
|
||||
descriptor.TextStyle.FontWeight = weight;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Thin<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.Thin);
|
||||
}
|
||||
|
||||
public static T ExtraLight<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.ExtraLight);
|
||||
}
|
||||
|
||||
public static T Light<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.Light);
|
||||
}
|
||||
|
||||
public static T NormalWeight<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.Normal);
|
||||
}
|
||||
|
||||
public static T Medium<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.Medium);
|
||||
}
|
||||
|
||||
public static T SemiBold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.SemiBold);
|
||||
}
|
||||
|
||||
public static T Bold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.Bold);
|
||||
}
|
||||
|
||||
public static T ExtraBold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.ExtraBold);
|
||||
}
|
||||
|
||||
public static T Black<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Weight(FontWeight.Black);
|
||||
}
|
||||
|
||||
public static T ExtraBlack<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
@@ -13,7 +14,13 @@ namespace QuestPDF.Fluent
|
||||
return style;
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
|
||||
public static TextStyle Color(this TextStyle style, string value)
|
||||
{
|
||||
return style.FontColor(value);
|
||||
}
|
||||
|
||||
public static TextStyle FontColor(this TextStyle style, string value)
|
||||
{
|
||||
return style.Mutate(x => x.Color = value);
|
||||
}
|
||||
@@ -23,12 +30,24 @@ namespace QuestPDF.Fluent
|
||||
return style.Mutate(x => x.BackgroundColor = value);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")]
|
||||
public static TextStyle FontType(this TextStyle style, string value)
|
||||
{
|
||||
return style.Mutate(x => x.FontType = value);
|
||||
return style.FontFamily(value);
|
||||
}
|
||||
|
||||
public static TextStyle FontFamily(this TextStyle style, string value)
|
||||
{
|
||||
return style.Mutate(x => x.FontFamily = value);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")]
|
||||
public static TextStyle Size(this TextStyle style, float value)
|
||||
{
|
||||
return style.FontSize(value);
|
||||
}
|
||||
|
||||
public static TextStyle FontSize(this TextStyle style, float value)
|
||||
{
|
||||
return style.Mutate(x => x.Size = value);
|
||||
}
|
||||
@@ -52,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
|
||||
|
||||
@@ -109,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
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
|
||||
{
|
||||
public static class Placeholders
|
||||
{
|
||||
public static readonly Random Random = new();
|
||||
public static readonly Random Random = new Random(0);
|
||||
|
||||
#region Word Cache
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
internal enum FontPosition
|
||||
{
|
||||
Normal,
|
||||
Subscript,
|
||||
Superscript,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user