Compare commits

...

93 Commits

Author SHA1 Message Date
MarcinZiabek bd577b9609 Implemented custom tree view with proper indendation 2022-07-18 10:47:07 +02:00
MarcinZiabek 100afbdc32 Prototype implementation 2022-07-12 15:21:50 +02:00
MarcinZiabek 2a0b133185 Fixed incorrect file name 2022-06-23 20:32:36 +02:00
Marcin Ziąbek 6058dac524 Merge pull request #249 from wieslawsoltes/CreateGlobalJson
Add global.json
2022-06-23 00:41:36 +02:00
Marcin Ziąbek 25b8f62245 Merge pull request #248 from wieslawsoltes/SetApplicationName
Set application name
2022-06-23 00:40:51 +02:00
MarcinZiabek 2c31f3d5b5 2022.6.2 2022-06-23 00:16:13 +02:00
Wiesław Šoltés f7cd89ccf6 Add global.json 2022-06-15 21:55:44 +02:00
Wiesław Šoltés 6914a9063a Set application name 2022-06-15 21:53:34 +02:00
MarcinZiabek b87e8d5d4e Improved exception messages 2022-06-14 17:53:16 +02:00
MarcinZiabek d740d67a64 2022.6.1 2022-06-13 00:17:32 +02:00
MarcinZiabek a14e9ca61f 2022.6 2022-06-12 23:48:38 +02:00
MarcinZiabek 44b3ff8d0d Merge branch 'text-shaping-new' 2022-06-08 02:46:54 +02:00
Marcin Ziąbek df6b88e14a Update readme.md 2022-06-08 02:10:52 +02:00
MarcinZiabek d4103797aa Improved exception message when SkiaSharp throws the TypeInitializationException 2022-06-08 02:01:58 +02:00
MarcinZiabek d894914e7e Improved Previewer stability 2022-06-08 00:09:59 +02:00
MarcinZiabek 90d1ce7af7 2022.6.0-prerelease 2022-05-31 00:52:49 +02:00
MarcinZiabek 5fa32a06cc Text: fixed corner case 2022-05-26 00:42:50 +02:00
MarcinZiabek acdf1ed1aa Stability improvement 2022-05-25 00:21:50 +02:00
MarcinZiabek a0b32e5ecf Fixed Row.AutoItem width calculation 2022-05-25 00:21:50 +02:00
MarcinZiabek 539dc83303 Fixed Row.AutoItem width calculation 2022-05-24 23:50:53 +02:00
MarcinZiabek d97fa2cef1 Minor optimization of SkFont creation 2022-05-23 01:18:21 +02:00
MarcinZiabek 6d122c784d Implemented working text shaping 2022-05-22 20:25:11 +02:00
MarcinZiabek 28743a81d1 Fixed typo 2022-05-21 14:35:13 +02:00
MarcinZiabek 32b94cea43 Updated sponsorship 2022-05-12 00:40:29 +02:00
Marcin Ziąbek 0ca55a91e0 Update readme.md 2022-05-10 21:48:39 +02:00
MarcinZiabek 09e6b5253f Fixed class names 2022-05-10 12:43:13 +02:00
MarcinZiabek bacb136a3f Benchmark: grid element 2022-05-10 12:42:49 +02:00
MarcinZiabek b7bc640e31 Added new example to dynamic component: footer with alternating position 2022-05-09 18:18:06 +02:00
MarcinZiabek 6e6f884a5f Updated sponsorship section 2022-05-09 16:33:12 +02:00
MarcinZiabek b47a8ac409 Added more examples 2022-05-09 16:32:59 +02:00
MarcinZiabek e51082400c Dynamic Component: added more examples 2022-05-09 14:07:50 +02:00
MarcinZiabek 52cfa3ab7e Added note regarding JetBrains OSS Power Ups 2022-05-07 22:18:14 +02:00
MarcinZiabek cd181b1e2e Merge branch '2022.5' 2022-05-07 21:40:10 +02:00
Marcin Ziąbek e71601ad0c Merge pull request #208 from danielchalmers/patch-1
replace obsolete method in readme example
2022-05-07 18:46:09 +02:00
Daniel Chalmers 3041aa53cc replace obsolete method in readme example
Warning	CS0618	'PageDescriptor.Background(string)' is obsolete: 'This element has been renamed since version 2022.3. Please use the PageColor method.'
2022-05-06 19:37:17 -05:00
MarcinZiabek 0ebf2833af Updated release notes 2022-05-06 21:17:16 +02:00
MarcinZiabek 25e6d9fbca Adjusted library versions 2022-05-06 21:09:16 +02:00
Marcin Ziąbek f80149792d Merge pull request #178 from jnyrup/Dispose
Explicit disposal of disposables
2022-05-06 20:59:35 +02:00
Marcin Ziąbek 2beee1d22e Update PreviewerService.cs 2022-05-06 20:59:06 +02:00
MarcinZiabek 1af7c0a144 Adjusted code related to subscript/superscript 2022-05-06 20:55:18 +02:00
Marcin Ziąbek eeca67e5bc Merge pull request #179 from Bebo-Maker/main
Implement subscript/superscript text
2022-05-06 00:09:31 +02:00
MarcinZiabek 4da3ef5dfa Fixed breaking long words 2022-05-05 23:52:17 +02:00
MarcinZiabek f3d4151273 Improved performance of the table rendering algorithm (square to linear complexity) 2022-05-05 23:20:23 +02:00
Marcin Ziąbek 9a7e397db2 Merge pull request #197 from knoxyz/patch-2
Replaced the obsolete methods from 2022.3 (FontType, Color and Size)
2022-05-05 18:23:11 +02:00
Marcin Ziąbek d81f21e658 Merge pull request #196 from knoxyz/patch-1
Update <TargetFramework> from 3.1 to net6.0
2022-05-05 18:11:21 +02:00
Marcin Ziąbek 78909c6edf Merge pull request #199 from knoxyz/patch-4
Replaced the obsolete methods from 2022.3 (InternalLink and Color)
2022-05-05 17:48:28 +02:00
Marcin Ziąbek a2d81b6adf Merge pull request #200 from knoxyz/patch-3
Replaced the obsolete methods from 2022.3 (ExternalLink and Location)
2022-05-05 17:47:40 +02:00
Marcin Ziąbek 4a761c023f Merge pull request #201 from simusr2/main
Replaced PageEnd with PageStart in PageNumberWithinSection
2022-05-05 17:46:30 +02:00
Simone Anile 24bbd06dff Replaced PageEnd with PageStart in PageNumberWithinSection 2022-04-29 15:19:58 +02:00
knoxyz 44bd646bab Replaced the obsolete methods from 2022.3 (InternalLink and Color) 2022-04-29 11:36:28 +02:00
knoxyz 5f0291b2c4 Replaced the obsolete methods from 2022.3 (ExternalLink and Location) 2022-04-29 11:32:20 +02:00
knoxyz 1b91015d8f Replaced the obsolete methods from 2022.3 (FontType, Color and Size) 2022-04-28 16:21:56 +02:00
knoxyz 2b6444463d Update <TargetFramework> from 3.1 to net6.0 2022-04-28 16:13:44 +02:00
MarcinZiabek a2629575e8 Dynamic: simplified container composition process 2022-04-27 09:50:17 +02:00
MarcinZiabek c8824021cc Dynamic: generalized dynamic element size measurement 2022-04-27 09:24:44 +02:00
MarcinZiabek e1101d870d Dynamic: improve state management 2022-04-27 06:56:13 +02:00
MarcinZiabek 152304b467 Dynamic: improved TextStyle handling 2022-04-26 01:11:40 +02:00
MarcinZiabek 8a1be90c4f Ported dynamic component implementation prototype 2022-04-26 00:36:50 +02:00
MarcinZiabek bc8a9e5dc5 Fixed mutating text style for page-related text items 2022-04-23 23:13:38 +02:00
Marcin Ziąbek e54e09de15 Merge pull request #185 from mikasius/main
Renamed logo.png
2022-04-23 23:03:37 +02:00
Sergey Metlenkin c49a424f87 Renamed logo.png
Otherwise solution won't build
2022-04-22 10:59:12 +07:00
Bebo-Maker c63b0658f3 Ignore FontPosition when calculating font metrics. 2022-04-12 19:14:00 +02:00
Bebo-Maker 82cd1d7835 Add example for superscript/subscript. 2022-04-12 18:49:34 +02:00
Bebo-Maker 3ffab6e933 Implement superscript and subscript. 2022-04-12 18:48:28 +02:00
Marcin Ziąbek e4d55e0298 Merge pull request #177 from jnyrup/ArgumentException
Use `paramName` overload of `ArgumentException`
2022-04-11 22:03:40 +02:00
Jonas Nyrup a6f4fd7f49 explicit disposal 2022-04-11 21:36:50 +02:00
Jonas Nyrup 048a4748b2 Use paramName overload of ArgumentException 2022-04-11 21:33:33 +02:00
Marcin Ziąbek 8cc2fb8696 Merge pull request #169 from fredericoregateiro/AvaloniaCompiledBindings
Enable compiled bindings on Previewer
2022-04-08 22:17:41 +02:00
MarcinZiabek 8e7b528cd9 Updated install command in readme 2022-04-08 22:15:12 +02:00
MarcinZiabek 26193313b8 Previewer: hide "Show in default browser" button when no document available 2022-04-08 22:14:19 +02:00
MarcinZiabek 555825e8be Previewer: throwing exception when cannot connect with frontend 2022-04-08 22:14:19 +02:00
Frederico Regateiro ddde8e7517 enable compiled bindings on PreviewerWindow 2022-04-06 14:08:53 +01:00
Marcin Ziąbek d2c82b5988 Update readme.md 2022-04-05 14:54:33 +02:00
Marcin Ziąbek d82aae95b5 Update readme.md 2022-04-05 14:34:00 +02:00
Marcin Ziąbek 5c11a2ab6a Update readme.md 2022-04-05 14:30:51 +02:00
MarcinZiabek 03e3a80fd9 Fixed logo resource for QuestPDF.Previwer 2022-04-04 23:26:16 +02:00
MarcinZiabek 4692c5ce62 Fixed QuestPDF.Previewer build 2022-04-04 23:24:02 +02:00
MarcinZiabek 919f916219 2022.4 release 2022-04-04 23:20:22 +02:00
MarcinZiabek 7a32f774a1 Fixed spelling 2022-04-04 00:38:58 +02:00
MarcinZiabek fba71e6392 Previewer: stop debugging session when previewer is closed 2022-04-04 00:19:36 +02:00
MarcinZiabek e27585a504 Fixed previewer: first document refresh sometimes does not work 2022-04-02 00:23:58 +02:00
MarcinZiabek 9be497ee41 Fixed automatically starting previewer application on custom port 2022-04-01 19:26:54 +02:00
MarcinZiabek 0fc0db55d2 Better button style path in previewer 2022-03-31 23:14:00 +02:00
MarcinZiabek 9e7a1ed49d Previewer improvements 2022-03-31 23:11:34 +02:00
MarcinZiabek c137bcfcde Improved look and feel 2022-03-31 18:33:34 +02:00
MarcinZiabek b80b3414d8 Added http-based previewer 2022-03-31 15:44:28 +02:00
MarcinZiabek 5f7cee3420 Removed old previewer implementation 2022-03-30 23:38:51 +02:00
MarcinZiabek 435c42ab9e Changed previewer communication layer to http 2022-03-30 23:37:37 +02:00
MarcinZiabek 7b4923b45d Merge remote-tracking branch 'origin/previewer' 2022-03-27 20:15:02 +02:00
MarcinZiabek 8732b7e50b Renaming 2022-03-27 20:12:22 +02:00
Marcin Ziąbek a0909eb057 Create FUNDING.yml 2022-03-26 21:06:44 +01:00
MarcinZiabek 11ff53946d Introduced new line breaking algorithm: break anywhere 2022-03-26 03:34:31 +01:00
MarcinZiabek 0711bca1e1 Improved respecting spaces at the beginning and end of text block, fixed breaking long words that do not fit in line 2022-03-26 02:58:55 +01:00
100 changed files with 3371 additions and 633 deletions
+1
View File
@@ -0,0 +1 @@
github: QuestPDF
+106
View File
@@ -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));
});
});
}
}
}
+42
View File
@@ -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}");
});
});
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
@@ -11,7 +11,7 @@
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
<PackageReference Include="Svg.Skia" Version="0.5.10" />
</ItemGroup>
+46
View File
@@ -0,0 +1,46 @@
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
namespace QuestPDF.Examples
{
public class TableBenchmark
{
[Test]
public void Benchmark()
{
RenderingTest
.Create()
.ProducePdf()
.PageSize(PageSizes.A4)
.ShowResults()
.MaxPages(10_000)
.EnableCaching(true)
.EnableDebugging(false)
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Table(table =>
{
const int numberOfRows = 100_000;
const int numberOfColumns = 10;
table.ColumnsDefinition(columns =>
{
foreach (var _ in Enumerable.Range(0, numberOfColumns))
columns.RelativeColumn();
});
foreach (var row in Enumerable.Range(0, numberOfRows))
foreach (var column in Enumerable.Range(0, numberOfColumns))
table.Cell().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}");
});
});
}
}
}
+1 -1
View File
@@ -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}");
+258 -12
View File
@@ -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,6 +27,27 @@ 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.FontSize(20));
@@ -36,13 +57,162 @@ namespace QuestPDF.Examples
});
});
}
[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 massenergy 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 =>
@@ -54,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}: ").SemiBold();
text.Line(Placeholders.Paragraph());
}
text.Line(Placeholders.Paragraph());
});
});
}
@@ -239,7 +403,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
text.DefaultTextStyle(TextStyle.Default.FontSize(20).BackgroundColor(Colors.Red.Lighten4));
text.AlignLeft();
text.ParagraphSpacing(10);
@@ -258,7 +422,7 @@ namespace QuestPDF.Examples
{
text.Line($"{i}: {Placeholders.Paragraph()}");
text.Hyperlink("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();
@@ -371,5 +535,87 @@ namespace QuestPDF.Examples
});
});
}
[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();
});
});
});
});
}
[Test]
public void TextShaping_Unicode()
{
RenderingTest
.Create()
.PageSize(600, 100)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(35)
.MinimalBox()
.Background(Colors.Grey.Lighten2)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
text.Span("Complex Unicode structure: ");
text.Span("T̶̖̔͆͆̽̔ḩ̷̼̫̐̈́̀͜͝͝ì̶͇̤͓̱̣͇͓͉̎s̵̡̟̹͍̜͉̗̾͛̈̐́͋͂͝͠ͅ ̴̨͙͍͇̭̒͗̀́͝ì̷̡̺͉̼̏̏̉̌͝s̷͍͙̗̰̖͙̈̑̂̔͑͊̌̓̊̇͜ ̶̛̼͚͊̅͘ṭ̷̨̘̣̙̖͉͌̏̂̅͑̄̽̕͝ȅ̶̲̲̙̭͈̬̣͔̝͔̈́͝s̸̢̯̪̫͓̭̮̓̀͆͜ț̸̢͉̞̥̤̏̌̓͝").FontColor(Colors.Red.Medium);
text.Span(".");
});
});
}
[Test]
public void TextShaping_Arabic()
{
RenderingTest
.Create()
.PageSize(500, 100)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.MinimalBox()
.Background(Colors.Grey.Lighten2)
.Text("ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا")
.FontSize(20);
});
}
}
}
+3 -21
View File
@@ -1,11 +1,11 @@
using Avalonia.Media;
using QuestPDF.Fluent;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
using QuestPDF.ReportSample;
using QuestPDF.ReportSample.Layouts;
using Colors = QuestPDF.Helpers.Colors;
//ImagePlaceholder.Solid = true;
var model = DataSource.GetReport();
var report = new StandardReport(model);
@@ -58,23 +58,5 @@ Document
x.CurrentPageNumber();
});
});
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.Red.Medium);
page.DefaultTextStyle(x => x.FontSize(20));
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
foreach (var i in Enumerable.Range(0, 10))
x.Item().Background(Colors.Grey.Lighten2).Height(80);
});
});
})
.ShowInPreviewer();
@@ -8,7 +8,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\QuestPDF.Previewer\QuestPDF.Previewer.csproj" />
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
</ItemGroup>
@@ -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();
}
}
@@ -1,50 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
public static class DocumentPreviewerExtensions
{
/// <summary>
/// Opens document in the QuestPDF previewer tool.
/// Improves development speed by supporting hot reloading.
/// Shows document preview and refreshes it after each code change.
/// </summary>
/// <remarks>
/// Intended for development only. Do not use in production environment.
/// </remarks>
public static void ShowInPreviewer(this IDocument document)
{
ArgumentNullException.ThrowIfNull(document);
// currently there is no way to utilize a previously run Avalonia app.
// so we need to check if the previewer was already run and show the window again.
if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new PreviewerWindow()
{
DataContext = new PreviewerWindowViewModel()
{
Document = document,
}
};
desktop.MainWindow.Show();
desktop.Start(Array.Empty<string>());
return;
}
AppBuilder
.Configure(() => new PreviewerApp()
{
Document = document,
})
.UsePlatformDetect()
.UseReactiveUI()
.StartWithClassicDesktopLifetime(Array.Empty<string>());
}
}
}
-63
View File
@@ -1,63 +0,0 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using Avalonia.Threading;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
internal class DocumentRenderer : INotifyPropertyChanged
{
public IDocument? Document { get; private set; }
public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<PreviewPage> _pages = new();
public ObservableCollection<PreviewPage> Pages
{
get => _pages;
set
{
if (_pages != value)
{
_pages = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pages)));
}
}
}
public void UpdateDocument(IDocument? document)
{
Document = document;
if (document == null)
return;
try
{
RenderDocument(document);
}
catch (Exception exception)
{
var exceptionDocument = new ExceptionDocument(exception);
RenderDocument(exceptionDocument);
}
}
private void RenderDocument(IDocument document)
{
var canvas = new PreviewerCanvas();
DocumentGenerator.RenderDocument(canvas, document);
foreach (var pages in Pages)
pages?.Picture?.Dispose();
Dispatcher.UIThread.Post(() =>
{
Pages.Clear();
Pages = new ObservableCollection<PreviewPage>(canvas.Pictures);
});
}
}
}
-55
View File
@@ -1,55 +0,0 @@
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer;
public class ExceptionDocument : IDocument
{
private Exception Exception { get; }
public ExceptionDocument(Exception exception)
{
Exception = exception;
}
public DocumentMetadata GetMetadata()
{
return DocumentMetadata.Default;
}
public void Compose(IDocumentContainer document)
{
document.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(1, Unit.Inch);
page.PageColor(Colors.Red.Lighten4);
page.DefaultTextStyle(x => x.FontSize(16));
page.Header()
.BorderBottom(2)
.BorderColor(Colors.Red.Medium)
.PaddingBottom(5)
.Text("Ooops! Something went wrong...").FontSize(28).FontColor(Colors.Red.Medium).Bold();
page.Content().PaddingVertical(20).Column(column =>
{
var currentException = Exception;
while (currentException != null)
{
column.Item().Text(currentException.GetType().Name).FontSize(20).SemiBold();
column.Item().Text(currentException.Message).FontSize(14);
column.Item().PaddingTop(10).Text(currentException.StackTrace).FontSize(10).Light();
currentException = currentException.InnerException;
if (currentException != null)
column.Item().PaddingVertical(15).LineHorizontal(2).LineColor(Colors.Red.Medium);
}
});
});
}
}
+20
View File
@@ -0,0 +1,20 @@
using SkiaSharp;
namespace QuestPDF.Previewer;
class Helpers
{
public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> pages)
{
using var stream = File.Create(filePath);
using var document = SKDocument.CreatePdf(stream);
foreach (var page in pages)
{
using var canvas = document.BeginPage(page.Width, page.Height);
canvas.DrawPicture(page.Picture);
document.EndPage();
}
}
}
+168 -29
View File
@@ -1,8 +1,8 @@
using System.Diagnostics;
using Avalonia;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using DynamicData;
using SkiaSharp;
namespace QuestPDF.Previewer;
@@ -11,9 +11,10 @@ class InteractiveCanvas : ICustomDrawOperation
{
public Rect Bounds { get; set; }
public ICollection<PreviewPage> Pages { get; set; }
public InspectionElement? InspectionElement { get; set; }
private float Width => (float)Bounds.Width;
private float Height => (float)Bounds.Height;
private float ViewportWidth => (float)Bounds.Width;
private float ViewportHeight => (float)Bounds.Height;
public float Scale { get; private set; } = 1;
public float TranslateX { get; set; }
@@ -25,11 +26,11 @@ class InteractiveCanvas : ICustomDrawOperation
private const float PageSpacing = 25f;
private const float SafeZone = 25f;
public float TotalPagesHeight => Pages.Sum(x => x.Size.Height) + (Pages.Count - 1) * PageSpacing;
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.Size.Width) : 0;
public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
public float MaxTranslateY => TotalHeight - Height / Scale;
public float MaxTranslateY => TotalHeight - ViewportHeight / Scale;
public float ScrollPercentY
{
@@ -47,7 +48,7 @@ class InteractiveCanvas : ICustomDrawOperation
{
get
{
var viewPortSize = Height / Scale / TotalHeight;
var viewPortSize = ViewportHeight / Scale / TotalHeight;
return Math.Clamp(viewPortSize, 0, 1);
}
}
@@ -62,19 +63,19 @@ class InteractiveCanvas : ICustomDrawOperation
private void LimitTranslate()
{
if (TotalPagesHeight > Height / Scale)
if (TotalPagesHeight > ViewportHeight / Scale)
{
TranslateY = Math.Min(TranslateY, MaxTranslateY);
TranslateY = Math.Max(TranslateY, 0);
}
else
{
TranslateY = (TotalPagesHeight - Height / Scale) / 2;
TranslateY = (TotalPagesHeight - ViewportHeight / Scale) / 2;
}
if (Width / Scale < MaxWidth)
if (ViewportWidth / Scale < MaxWidth)
{
var maxTranslateX = (Width / 2 - SafeZone) / Scale - MaxWidth / 2;
var maxTranslateX = (ViewportWidth / 2 - SafeZone) / Scale - MaxWidth / 2;
TranslateX = Math.Min(TranslateX, -maxTranslateX);
TranslateX = Math.Max(TranslateX, maxTranslateX);
@@ -99,14 +100,78 @@ class InteractiveCanvas : ICustomDrawOperation
Scale *= factor;
LimitScale();
factor = Scale / oldScale;
TranslateX -= x / (oldScale * factor) - x / oldScale;
TranslateY -= y / (oldScale * factor) - y / oldScale;
TranslateX -= x / Scale - x / oldScale;
TranslateY -= y / Scale - y / oldScale;
LimitTranslate();
}
public int ActivePage { get; set; } = 1;
public IEnumerable<(int pageNumber, float beginY, float endY)> GetPagePosition()
{
var pageNumber = 1;
var currentPagePosition = SafeZone / Scale;
foreach (var page in Pages)
{
yield return (pageNumber, currentPagePosition, currentPagePosition + page.Height);
currentPagePosition += page.Height + PageSpacing;
pageNumber++;
}
}
public void SetActivePage(float x, float y)
{
y /= Scale;
y += TranslateY;
ActivePage = GetPagePosition().FirstOrDefault(p => p.beginY <= y && y <= p.endY).pageNumber;
}
public void ScrollToInspectionElement(InspectionElement element)
{
var location = element.Location.MinBy(x => x.PageNumber);
var pagePosition = GetPagePosition().ElementAt(location.PageNumber - 1);
var page = Pages.ElementAt(location.PageNumber - 1);
var widthScale = ViewportWidth / location.Width;
var heightScale = ViewportHeight / location.Height;
var targetScale = Math.Min(widthScale, heightScale);
targetScale *= 0.7f; // slightly zoom out to show entire element with padding
Scale = targetScale;
TranslateY = pagePosition.beginY + location.Top + location.Height / 2 - ViewportHeight / Scale / 2;
TranslateX = page.Width / 2 - location.Left - location.Width / 2;
}
public (int pageNumber, float x, float y)? FindClickedPointOnThePage(float x, float y)
{
x -= ViewportWidth / 2;
x /= Scale;
x += TranslateX;
y /= Scale;
y += TranslateY;
var location = GetPagePosition().FirstOrDefault(p => p.beginY <= y && y <= p.endY);
if (location == default)
return null;
var page = Pages.ElementAt(location.pageNumber - 1);
x += page.Width / 2;
if (x < 0 || page.Width < x)
return null;
y -= location.beginY;
return (location.pageNumber, x, y);
}
#endregion
@@ -127,20 +192,32 @@ class InteractiveCanvas : ICustomDrawOperation
var originalMatrix = canvas.TotalMatrix;
canvas.Translate(Width / 2, 0);
canvas.Translate(ViewportWidth / 2, 0);
canvas.Scale(Scale);
canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
canvas.Translate(TranslateX, -TranslateY);
var topMatrix = canvas.TotalMatrix;;
var positions = GetPagePosition().ToList();
foreach (var page in Pages)
foreach (var pageIndex in Enumerable.Range(0, Pages.Count))
{
canvas.Translate(-page.Size.Width / 2f, 0);
DrawBlankPage(canvas, page.Size);
canvas.SetMatrix(topMatrix);
var page = Pages.ElementAt(pageIndex);
var position = positions.ElementAt(pageIndex);
canvas.Translate(-page.Width / 2f, position.beginY);
DrawBlankPage(canvas, page.Width, page.Height);
canvas.DrawPicture(page.Picture);
canvas.Translate(page.Size.Width / 2f, page.Size.Height + PageSpacing);
DrawInspectionElement(canvas, pageIndex + 1);
}
canvas.SetMatrix(topMatrix);
DrawActivePage(canvas);
canvas.SetMatrix(originalMatrix);
DrawInnerGradient(canvas);
}
@@ -165,10 +242,10 @@ class InteractiveCanvas : ICustomDrawOperation
SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32)))
};
private void DrawBlankPage(SKCanvas canvas, QuestPDF.Infrastructure.Size size)
private void DrawBlankPage(SKCanvas canvas, float width, float height)
{
canvas.DrawRect(0, 0, size.Width, size.Height, BlankPageShadowPaint);
canvas.DrawRect(0, 0, size.Width, size.Height, BlankPagePaint);
canvas.DrawRect(0, 0, width, height, BlankPageShadowPaint);
canvas.DrawRect(0, 0, width, height, BlankPagePaint);
}
#endregion
@@ -176,7 +253,7 @@ class InteractiveCanvas : ICustomDrawOperation
#region inner viewport gradient
private const int InnerGradientSize = (int)SafeZone;
private static readonly SKColor InnerGradientColor = SKColor.Parse("#666");
private static readonly SKColor InnerGradientColor = SKColor.Parse("#555");
private void DrawInnerGradient(SKCanvas canvas)
{
@@ -198,7 +275,69 @@ class InteractiveCanvas : ICustomDrawOperation
SKShaderTileMode.Clamp)
};
canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
canvas.DrawRect(0, 0, ViewportWidth, InnerGradientSize, fogPaint);
}
#endregion
#region Interactivity
private void DrawActivePage(SKCanvas canvas)
{
if (ActivePage == default)
return;
var page = Pages.ElementAt(ActivePage - 1);
var pagePosition = GetPagePosition().ElementAt(ActivePage - 1);
var thickness = 6f / Scale;
using var strokePaint = new SKPaint
{
StrokeWidth = thickness,
IsStroke = true,
Color = SKColor.Parse("#000")
};
canvas.DrawRect(- page.Width / 2 -thickness / 2, pagePosition.beginY -thickness / 2, page.Width + thickness, page.Height + thickness, strokePaint);
}
private void DrawInspectionElement(SKCanvas canvas, int pageNumber)
{
if (InspectionElement == null || InspectionElement.Location == null)
return;
var location = InspectionElement.Location.FirstOrDefault(x => x.PageNumber == pageNumber);
if (location == null)
return;
var size = 6 / Scale;
size = Math.Min(size, 3);
using var strokePaint1 = new SKPaint
{
StrokeWidth = size,
IsStroke = true,
Color = SKColor.Parse("#42A5F5")
};
using var strokePaint2 = new SKPaint
{
StrokeWidth = size,
IsStroke = true,
PathEffect = SKPathEffect.CreateDash(new[] { size * 3, size * 3 }, 0),
Color = SKColor.Parse("#1E88E5")
};
using var backgroundPaint = new SKPaint
{
Color = SKColor.Parse("#442196F3"),
};
canvas.DrawRect(location.Left, location.Top, location.Width, location.Height, backgroundPaint);
canvas.DrawRect(location.Left + size / 2, location.Top + size / 2, location.Width - size, location.Height - size, strokePaint1);
canvas.DrawRect(location.Left + size / 2, location.Top + size / 2, location.Width - size, location.Height - size, strokePaint2);
}
#endregion
+45
View File
@@ -0,0 +1,45 @@
<UserControl 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.MyHierarchy">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Panel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Name="Highlight" Background="transparent" DoubleTapped="InputElement_OnDoubleTapped" PointerEnter="Highlight_OnPointerEnter" PointerLeave="Indentation_OnPointerLeave" PointerPressed="Clicked"></Panel>
<Panel Grid.Row="0" Grid.Column="0" Name="Indentation"></Panel>
<Panel Grid.Row="0" Grid.Column="1" Name="Panel" Width="16" Height="16" Margin="0,0,8,0" Background="transparent" PointerPressed="Toggle" PointerEnter="Highlight_OnPointerEnter" PointerLeave="Indentation_OnPointerLeave">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="#AFFF" Name="Icon" />
</Canvas>
</Viewbox>
</Panel>
<Panel Grid.Row="0" Grid.Column="2">
<TextBlock Name="ElementName" FontSize="12" Foreground="#AFFF" Margin="0,6" IsHitTestVisible="False"></TextBlock>
</Panel>
<ItemsRepeater Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Name="Repeater" HorizontalCacheLength="1000000" VerticalCacheLength="100000">
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="previewer:InspectionElement">
<previewer:MyHierarchy Hierarchy="{Binding}" OnSelected="MyHierarchy_OnOnSelected"></previewer:MyHierarchy>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Grid>
</UserControl>
+115
View File
@@ -0,0 +1,115 @@
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Color = System.Drawing.Color;
namespace QuestPDF.Previewer
{
public partial class MyHierarchy : UserControl
{
public static readonly StyledProperty<InspectionElement> HierarchyProperty =
AvaloniaProperty.Register<MyHierarchy, InspectionElement>(nameof(Hierarchy));
public InspectionElement Hierarchy
{
get => GetValue(HierarchyProperty);
set => SetValue(HierarchyProperty, value);
}
public static readonly StyledProperty<InspectionElement?> SelectionProperty =
AvaloniaProperty.Register<MyHierarchy, InspectionElement?>(nameof(Selection));
public InspectionElement? Selection
{
get => GetValue(SelectionProperty);
set => SetValue(SelectionProperty, value);
}
public event Action<InspectionElement>? OnSelected;
public bool Extended { get; set; }
public MyHierarchy()
{
InitializeComponent();
HierarchyProperty.Changed.Subscribe(x =>
{
Configure();
});
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
Configure();
}
private void Configure()
{
var panel = this.FindControl<Panel>("Panel");
panel.IsVisible = Hierarchy?.Children?.Any() ?? false;
this.FindControl<Panel>("Indentation").Width = (Hierarchy?.Level ?? 0) * 24;
UpdateIcon();
this.FindControl<TextBlock>("ElementName").Text = Hierarchy?.Text;
this.FindControl<TextBlock>("ElementName").Foreground = new SolidColorBrush(Avalonia.Media.Color.Parse(Hierarchy?.FontColor ?? "#FFF"));
}
private const string ExtendedIcon = "M7,15L12,10L17,15H7Z";
private const string CollapsedIcon = "M7,10L12,15L17,10H7Z";
private void Toggle(object? sender, PointerPressedEventArgs e)
{
Extended = !Extended;
UpdateIcon();
this.FindControl<ItemsRepeater>("Repeater").Items = Extended ? Hierarchy?.Children : Array.Empty<InspectionElement>();
}
private void UpdateIcon()
{
var iconControl = this.FindControl<Avalonia.Controls.Shapes.Path>("Icon");
iconControl.Data = new PathGeometry
{
Figures = PathFigures.Parse(Extended ? ExtendedIcon : CollapsedIcon)
};
}
private void InputElement_OnDoubleTapped(object? sender, RoutedEventArgs e)
{
Toggle(null, null);
}
private void Highlight_OnPointerEnter(object? sender, PointerEventArgs e)
{
Cursor = Cursor.Parse("hand");
this.FindControl<Panel>("Highlight").Background = new SolidColorBrush(Avalonia.Media.Color.Parse("#1FFF"));
}
private void Indentation_OnPointerLeave(object? sender, PointerEventArgs e)
{
Cursor = Cursor.Default;
this.FindControl<Panel>("Highlight").Background = new SolidColorBrush(Avalonia.Media.Color.Parse("#0000"));
}
private void Clicked(object? sender, PointerPressedEventArgs e)
{
OnSelected?.Invoke(Hierarchy);
}
private void MyHierarchy_OnOnSelected(InspectionElement selected)
{
OnSelected?.Invoke(selected);
Selection = selected;
}
}
}
+6
View File
@@ -0,0 +1,6 @@
using SkiaSharp;
namespace QuestPDF.Previewer
{
record PreviewPage(SKPicture Picture, float Width, float Height);
}
+2 -1
View File
@@ -1,6 +1,7 @@
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="QuestPDF Document Preview">
<Application.Styles>
<FluentTheme Mode="Dark" />
</Application.Styles>
-6
View File
@@ -1,14 +1,11 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
internal class PreviewerApp : Application
{
public IDocument? Document { get; init; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -21,9 +18,6 @@ namespace QuestPDF.Previewer
desktop.MainWindow = new PreviewerWindow()
{
DataContext = new PreviewerWindowViewModel()
{
Document = Document,
}
};
}
+81 -2
View File
@@ -1,10 +1,8 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using ReactiveUI;
namespace QuestPDF.Previewer
{
@@ -37,6 +35,22 @@ namespace QuestPDF.Previewer
set => SetValue(ScrollViewportSizeProperty, value);
}
public static readonly StyledProperty<InspectionElement> HierarchyProperty = AvaloniaProperty.Register<PreviewerControl, InspectionElement>(nameof(Hierarchy));
public InspectionElement Hierarchy
{
get => GetValue(HierarchyProperty);
set => SetValue(HierarchyProperty, value);
}
public static readonly StyledProperty<InspectionElement> CurrentSelectionProperty = AvaloniaProperty.Register<PreviewerControl, InspectionElement>(nameof(CurrentSelection));
public InspectionElement CurrentSelection
{
get => GetValue(CurrentSelectionProperty);
set => SetValue(CurrentSelectionProperty, value);
}
public PreviewerControl()
{
PagesProperty.Changed.Subscribe(x =>
@@ -51,7 +65,72 @@ namespace QuestPDF.Previewer
InvalidateVisual();
});
CurrentSelectionProperty.Changed.Subscribe(x =>
{
InteractiveCanvas.InspectionElement = CurrentSelection;
//InteractiveCanvas.ScrollToInspectionElement(CurrentSelection);
InvalidateVisual();
});
ClipToBounds = true;
PointerPressed += (sender, args) =>
{
var position = args.GetPosition(this);
InteractiveCanvas.SetActivePage((float)position.X, (float)position.Y);
var clickedPosition = InteractiveCanvas.FindClickedPointOnThePage((float)position.X, (float)position.Y);
if (clickedPosition != null)
FindHighlightedElement(clickedPosition.Value.pageNumber, clickedPosition.Value.x, clickedPosition.Value.y);
InvalidateVisual();
};
}
void FindHighlightedElement(int pageNumber, float x, float y)
{
var possible = FlattenHierarchy(Hierarchy, 0)
.Select(x =>
{
var location = x.element.Location.First(y => y.PageNumber == pageNumber);
return new
{
Element = x.element,
Level = x.level,
Size = location.Width * location.Height
};
})
.ToList();
var minSize = possible.Min(x => x.Size);
CurrentSelection = possible
.Where(x => Math.Abs(x.Size - minSize) < 1)
.OrderByDescending(x => x.Level)
.First()
.Element;
IEnumerable<(InspectionElement element, int level)> FlattenHierarchy(InspectionElement element, int level)
{
var location = element.Location.FirstOrDefault(x => x.PageNumber == pageNumber);
if (location == null)
yield break;
if (x < location.Left || location.Left + location.Width < x)
yield break;
if (y < location.Top || location.Top + location.Height < y)
yield break;
yield return (element, level);
foreach (var childIndex in Enumerable.Range(0, element.Children.Count))
foreach (var nestedChild in FlattenHierarchy(element.Children[childIndex], level + childIndex + 1))
yield return nestedChild;
}
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
@@ -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; }
}
}
+136 -63
View File
@@ -1,82 +1,155 @@
<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"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="true"
ExtendClientAreaTitleBarHeightHint="-1"
Background="#666"
Icon="/Images/Logo.png"
UseLayoutRounding="True"
Title="QuestPDF Document Preview">
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"
xmlns:visualBasic="clr-namespace:Microsoft.VisualBasic;assembly=Microsoft.VisualBasic.Core"
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="56"
Background="#555"
Icon="/Resources/Logo.png"
UseLayoutRounding="True"
Title="QuestPDF Document Preview">
<Window.Styles>
<Style Selector=".MainMenu TextBlock">
<Setter Property="Foreground" Value="#AFFF" />
<Setter Property="Margin" Value="0,0,16,0" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector=".MainMenu TextBlock:pointerover">
<Setter Property="Foreground" Value="#FFF" />
<Setter Property="TextDecorations" Value="Underline" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Button.actions">
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Padding" Value="12" />
<Setter Property="Margin" Value="-1" />
<Setter Property="Background" Value="transparent"/>
</Style>
<Style Selector="Button.actions Path">
<Setter Property="Fill" Value="#8FFF"/>
</Style>
<Style Selector="Button.active Path">
<Setter Property="Fill" Value="#FFFF"/>
</Style>
<Style Selector="Button:pointerover Path">
<Setter Property="Fill" Value="#FFFF"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#333"/>
</Style>
</Window.Styles>
<Panel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="56" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="300" MaxWidth="500" Width="300" />
<ColumnDefinition Width="4" />
<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"
<Panel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Background="#333">
<StackPanel Orientation="Horizontal">
<Image Source="/Resources/Logo.png" Width="32" Height="32" Margin="12,12,20,12"></Image>
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<TextBlock
VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,0,4"
TextAlignment="Center" Text="QuestPDF Previewer" FontSize="16" Foreground="#FFF" FontWeight="SemiBold" />
<StackPanel Orientation="Horizontal" Classes="MainMenu">
<TextBlock Text="Getting Started" />
<TextBlock Text="Documentation" />
<TextBlock Text="GitHub" />
<TextBlock Text="Sponsor project" />
</StackPanel>
</StackPanel>
</StackPanel>
</Panel>
<Grid Grid.Row="1" Grid.Column="0" Background="#444">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Panel Grid.Row="0">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Margin="16,8"
TextAlignment="Center" Text="Document hierarchy" Foreground="#DFFF" FontWeight="SemiBold" IsHitTestVisible="False" />
</Panel>
<Panel Grid.Row="1">
<ScrollViewer>
<previewer:MyHierarchy Hierarchy="{Binding Items}" Margin="-8,0,0,0" Selection="{Binding SelectedItem, Mode=TwoWay}"></previewer:MyHierarchy>
</ScrollViewer>
</Panel>
<Panel Grid.Row="3">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Margin="16,8"
TextAlignment="Center" Text="Selected element properties" Foreground="#DFFF" FontWeight="SemiBold" IsHitTestVisible="False" />
</Panel>
<Panel Grid.Row="4">
<ItemsRepeater Items="{Binding SelectedItem.Metadata}" Margin="16, 2">
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="previewer:Metadata">
<Grid Margin="0, 4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Label}" Foreground="#AFFF" FontSize="12" />
<TextBlock Grid.Column="2" Text="{Binding Value}" Foreground="#AFFF" FontSize="12" />
</Grid>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Panel>
</Grid>
<GridSplitter Grid.Column="1" Grid.Row="1" Background="#444" ResizeDirection="Columns" />
<previewer:PreviewerControl Grid.Row="1" Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
Pages="{Binding DocumentRenderer.Pages, Mode=OneWay}" />
CurrentSelection="{Binding SelectedItem, Mode=TwoWay}"
Hierarchy="{Binding Items}"
Pages="{Binding Pages, Mode=OneWay}" />
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32" ZIndex="100">
<Button VerticalAlignment="Bottom" HorizontalAlignment="Left"
Padding="10" CornerRadius="100"
Command="{Binding ShowPdfCommand, Mode=OneTime}"
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 VerticalAlignment="Bottom" HorizontalAlignment="Left"
Padding="10" CornerRadius="100"
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 VerticalAlignment="Bottom" HorizontalAlignment="Left"
Padding="10" CornerRadius="100"
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>
<ScrollBar Grid.Row="1" Grid.Column="3"
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>
+3 -2
View File
@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace QuestPDF.Previewer
@@ -12,7 +13,7 @@ namespace QuestPDF.Previewer
protected override void OnClosed(EventArgs e)
{
(DataContext as PreviewerWindowViewModel)?.UnregisterHotReloadHandler();
}
private void InitializeComponent()
+107 -38
View File
@@ -1,7 +1,8 @@
using QuestPDF.Fluent;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text.Json;
using Avalonia;
using ReactiveUI;
using QuestPDF.Infrastructure;
using Unit = System.Reactive.Unit;
using Avalonia.Threading;
@@ -9,19 +10,13 @@ namespace QuestPDF.Previewer
{
internal class PreviewerWindowViewModel : ReactiveObject
{
public DocumentRenderer DocumentRenderer { get; } = new();
private IDocument? _document;
public IDocument? Document
private ObservableCollection<PreviewPage> _pages = new();
public ObservableCollection<PreviewPage> Pages
{
get => _document;
set
{
this.RaiseAndSetIfChanged(ref _document, value);
UpdateDocument(value);
}
get => _pages;
set => this.RaiseAndSetIfChanged(ref _pages, value);
}
private float _currentScroll;
public float CurrentScroll
{
@@ -53,47 +48,26 @@ namespace QuestPDF.Previewer
public PreviewerWindowViewModel()
{
HotReloadManager.UpdateApplicationRequested += InvalidateDocument;
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"));
}
public void UnregisterHotReloadHandler()
{
HotReloadManager.UpdateApplicationRequested -= InvalidateDocument;
}
private void InvalidateDocument(object? sender, EventArgs e)
{
UpdateDocument(Document);
}
private Task UpdateDocument(IDocument? document)
{
return Task.Run(() => DocumentRenderer.UpdateDocument(document));
LoadItems();
}
private void ShowPdf()
{
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf");
try
{
Document?.GeneratePdf(filePath);
}
catch (Exception exception)
{
new ExceptionDocument(exception).GeneratePdf(filePath);
}
Helpers.GeneratePdfFromDocumentSnapshots(filePath, Pages);
OpenLink(filePath);
}
private void OpenLink(string path)
{
var openBrowserProcess = new Process
using var openBrowserProcess = new Process
{
StartInfo = new()
{
@@ -104,5 +78,100 @@ namespace QuestPDF.Previewer
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();
}
private InspectionElement _selectedItem;
public InspectionElement SelectedItem
{
get => _selectedItem;
set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
}
private InspectionElement items;
public InspectionElement Items
{
get => items;
set => this.RaiseAndSetIfChanged(ref items, value);
}
public void LoadItems()
{
var text = File.ReadAllText("hierarchy.json");
Items = JsonSerializer.Deserialize<InspectionElement>(text);
UpdateLevel(Items);
}
private void UpdateLevel(InspectionElement root)
{
var currentLevel = 1;
Traverse(root);
void Traverse(InspectionElement element)
{
element.Level = currentLevel;
currentLevel++;
element.Children.ForEach(Traverse);
currentLevel--;
}
}
}
public class InspectionElementLocation
{
public int PageNumber { get; set; }
public float Top { get; set; }
public float Left { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
public class Metadata
{
public string Label { get; set; }
public string Value { get; set; }
public Metadata(string label, string value)
{
Label = label;
Value = value;
}
}
public class InspectionElement
{
public string Element { get; set; }
public List<InspectionElementLocation> Location { get; set; }
public Dictionary<string, string> Properties { get; set; }
public List<InspectionElement> Children { get; set; }
public int Level { get; set; }
public string FontColor => Element == "DebugPointer" ? "#FFF" : "#AFFF";
public string Text => Element == "DebugPointer" ? Properties.First(x => x.Key == "Target").Value : Element;
public bool Expanded { get; set; }
public IList<Metadata> Metadata => ListMetadata().ToList();
public IEnumerable<Metadata> ListMetadata()
{
yield return new Metadata("Element name", Element);
yield return new Metadata("Position left", Location[0].Left.ToString("N2"));
yield return new Metadata("Position top", Location[0].Top.ToString("N2"));
yield return new Metadata("Width", Location[0].Width.ToString("N2"));
yield return new Metadata("Height", Location[0].Height.ToString("N2"));
foreach (var property in Properties)
yield return new Metadata(property.Key, property.Value);
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI;
using QuestPDF.Previewer;
var applicationPort = GetCommunicationPort();
CommunicationService.Instance.Start(applicationPort);
if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new PreviewerWindow()
{
DataContext = new PreviewerWindowViewModel()
};
desktop.MainWindow.Show();
desktop.Start(Array.Empty<string>());
return;
}
AppBuilder
.Configure(() => new PreviewerApp())
.UsePlatformDetect()
.UseReactiveUI()
.StartWithClassicDesktopLifetime(Array.Empty<string>());
static int GetCommunicationPort()
{
const int defaultApplicationPort = 12500;
var arguments = Environment.GetCommandLineArgs();
if (arguments.Length < 2)
return defaultApplicationPort;
return int.TryParse(arguments[1], out var port) ? port : defaultApplicationPort;
}
+54 -23
View File
@@ -1,27 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<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="Images\Logo.png" />
</ItemGroup>
<ItemGroup>
<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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
</ItemGroup>
<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>
<None Update="hierarchy.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</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="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.80.4" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
</ItemGroup>
</Project>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long
+23
View File
@@ -0,0 +1,23 @@
Install nuget locally (in directory where nupkg file is located)
```
dotnet tool install --global --add-source . QuestPDF.Previewer --global
```
Run on default port
```
questpdf-previewer
```
Run on custom port
```
questpdf-previewer 12500
```
Remove nuget locally
```
dotnet tool uninstall QuestPDF.Previewer --global
```
@@ -55,7 +55,7 @@ namespace QuestPDF.ReportSample.Layouts
row.Spacing(50);
row.RelativeItem().PaddingTop(-10).Text(Model.Title).Style(Typography.Title);
row.ConstantItem(90).ExternalLink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
row.ConstantItem(90).Hyperlink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
});
column.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal();
@@ -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
{
@@ -41,18 +42,31 @@ 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(" (").Style(lengthStyle);
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => x == 1 ? "1 page long" : $"{x} pages long");
@@ -61,4 +75,4 @@ namespace QuestPDF.ReportSample.Layouts
});
}
}
}
}
@@ -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>
@@ -12,7 +12,7 @@
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
</ItemGroup>
<ItemGroup>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+4 -4
View File
@@ -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 -1
View File
@@ -10,7 +10,7 @@
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
</ItemGroup>
<ItemGroup>
+2 -2
View File
@@ -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();
-18
View File
@@ -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.FontFamily, style.FontFamily, "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));
+6 -6
View File
@@ -8,8 +8,6 @@ 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", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}"
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}"
@@ -17,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configurat
.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
@@ -39,13 +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
{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.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
-7
View File
@@ -1,7 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("QuestPDF.Previewer")]
[assembly: InternalsVisibleTo("QuestPDF.UnitTests")]
[assembly: InternalsVisibleTo("QuestPDF.Examples")]
[assembly: InternalsVisibleTo("QuestPDF.ReportSample")]
+1
View File
@@ -16,6 +16,7 @@ namespace QuestPDF.Drawing
var container = new Container();
container
.DebugPointer("Document")
.Column(column =>
{
Pages
+237 -7
View File
@@ -1,7 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
@@ -10,6 +12,8 @@ using QuestPDF.Elements.Text.Items;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace QuestPDF.Drawing
{
@@ -51,6 +55,13 @@ namespace QuestPDF.Drawing
return canvas.Images;
}
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
{
@@ -68,9 +79,218 @@ namespace QuestPDF.Drawing
ApplyCaching(content);
RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
if (metadata.ApplyInspection)
ApplyInspection(content);
RenderPass(pageContext, canvas, content, metadata, debuggingState);
if (metadata.ApplyInspection)
{
var x = TraverseStatisticsToJson(content);
var y = JsonSerializer.Serialize(x);
}
}
internal static string TraverseStructure(Element container)
{
var builder = new StringBuilder();
var nestingLevel = 0;
Traverse(container);
return builder.ToString();
void Traverse(Element item)
{
var indent = new string(' ', nestingLevel * 4);
var title = item.GetType().Name;
builder.AppendLine(indent + title);
nestingLevel++;
foreach (var child in item.GetChildren())
Traverse(child);
nestingLevel--;
}
}
internal static string TraverseStatistics(Element container, int pageNumber)
{
var builder = new StringBuilder();
var nestingLevel = 0;
Traverse(container);
return builder.ToString();
void Traverse(Element item)
{
if (item is DebuggingProxy or CacheProxy or Container)
{
Traverse(item.GetChildren().First());
return;
}
var inspectionItem = item as InspectionProxy;
if (inspectionItem == null)
{
var children = item.GetChildren().ToList();
if (children.Count > 1)
nestingLevel++;
children.ForEach(Traverse);
if (children.Count > 1)
nestingLevel--;
return;
}
if (!inspectionItem.Statistics.ContainsKey(pageNumber))
return;
var statistics = inspectionItem.Statistics[pageNumber];
var indent = new string(' ', nestingLevel * 4);
var title = statistics.Element.GetType().Name;
builder.AppendLine(indent + title);
builder.AppendLine(indent + new string('-', title.Length));
builder.AppendLine(indent + "Size: " + statistics.Size);
builder.AppendLine(indent + "Position: " + statistics.Position);
foreach (var configuration in DebuggingState.GetElementConfiguration(statistics.Element))
builder.AppendLine(indent + configuration);
builder.AppendLine();
Traverse(inspectionItem.Child);
}
}
internal static InspectionElement TraverseStatisticsToJson(Element container)
{
return Traverse(container);
InspectionElement? Traverse(Element item)
{
InspectionElement? result = null;
Element currentItem = item;
while (true)
{
if (currentItem is InspectionProxy proxy)
{
if (proxy.Child.GetType() == typeof(Container))
{
currentItem = proxy.Child;
continue;
}
var statistics = GetInspectionElement(proxy);
if (statistics == null)
return null;
if (result == null)
{
result = statistics;
}
else
{
result.Children.Add(statistics);
}
currentItem = proxy.Child;
}
else
{
var children = currentItem.GetChildren().ToList();
if (children.Count == 0)
{
return result;
}
else if (children.Count == 1)
{
currentItem = children.First();
continue;
}
else
{
children
.Select(Traverse)
.Where(x => x != null)
.ToList()
.ForEach(result.Children.Add);
return result;
}
}
}
}
static InspectionElement? GetInspectionElement(InspectionProxy inspectionProxy)
{
var locations = inspectionProxy
.Statistics
.Keys
.Select(x =>
{
var statistics = inspectionProxy.Statistics[x];
return new InspectionElementLocation
{
PageNumber = x,
Top = statistics.Position.Y,
Left = statistics.Position.X,
Width = statistics.Size.Width,
Height = statistics.Size.Height,
};
})
.ToList();
return new InspectionElement
{
Element = inspectionProxy.Child.GetType().Name,
Location = locations,
Properties = GetElementConfiguration(inspectionProxy.Child),
Children = new List<InspectionElement>()
};
}
static Dictionary<string, string> GetElementConfiguration(IElement element)
{
return element
.GetType()
.GetProperties()
.Select(x => new
{
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.ToDictionary(x => x.Property, x => FormatValue(x.Value));
string FormatValue(object value)
{
const int maxLength = 100;
var text = value?.ToString() ?? "-";
if (text.Length < maxLength)
return text;
return text.AsSpan(0, maxLength).ToString() + "...";
}
}
}
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState)
where TCanvas : ICanvas, IRenderingCanvas
{
@@ -128,11 +348,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);
}
}
@@ -151,13 +370,21 @@ namespace QuestPDF.Drawing
content.VisitChildren(x =>
{
x.CreateProxy(y => new DebuggingProxy(debuggingState, y));
x.CreateProxy(y => y is ElementProxy ? y : new DebuggingProxy(debuggingState, y));
});
return debuggingState;
}
private static void ApplyInspection(Container content)
{
content.VisitChildren(x =>
{
x.CreateProxy(y => y is ElementProxy ? y : new InspectionProxy(y));
});
}
private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
{
if (content == null)
return;
@@ -179,6 +406,9 @@ namespace QuestPDF.Drawing
return;
}
if (content is DynamicHost dynamicHost)
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
var targetTextStyle = documentDefaultTextStyle;
if (content is DefaultTextStyle defaultTextStyleElement)
+1
View File
@@ -26,6 +26,7 @@ namespace QuestPDF.Drawing
public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
public bool ApplyInspection { get; set; } = System.Diagnostics.Debugger.IsAttached;
public static DocumentMetadata Default => new DocumentMetadata();
}
@@ -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,36 @@
using System;
namespace QuestPDF.Drawing.Exceptions
{
public class InitializationException : Exception
{
internal InitializationException(string documentType, Exception innerException) : base(CreateMessage(documentType, innerException.Message), innerException)
{
}
private static string CreateMessage(string documentType, string innerExceptionMessage)
{
var (libraryName, nugetConvention) = GetLibraryName();
return $"Cannot create the {documentType} document using the {libraryName} library. " +
$"This exception usually means that, on your operating system where you run the application, {libraryName} requires installing additional dependencies. " +
$"Such dependencies are available as additional nuget packages, for example {nugetConvention}.Linux. " +
$"Some operating systems may require installing multiple nugets, e.g. MacOS may need both {nugetConvention}.macOS and {nugetConvention}.Linux." +
$"Please refer to the {libraryName} documentation for more details. " +
$"Also, please consult the inner exception that has been originally thrown by the dependency library.";
(string GetLibraryName, string nugetConvention) GetLibraryName()
{
if (innerExceptionMessage.Contains("libSkiaSharp"))
return ("SkiaSharp", "SkiaSharp.NativeAssets.Linux");
if (innerExceptionMessage.Contains("libHarfBuzzSharp"))
return ("HarfBuzzSharp", "HarfBuzzSharp.NativeAssets");
// default
return ("SkiaSharp-related", "*.NativeAssets.Linux");
}
}
}
}
+66 -6
View File
@@ -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)
{
@@ -46,7 +52,7 @@ 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)
{
@@ -60,7 +66,7 @@ namespace QuestPDF.Drawing
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)
{
@@ -68,7 +74,7 @@ namespace QuestPDF.Drawing
{
Color = SKColor.Parse(style.Color),
Typeface = GetTypeface(style),
TextSize = style.Size ?? 12,
TextSize = (style.Size ?? 12) * GetTextScale(style),
IsAntialias = true,
};
}
@@ -76,6 +82,16 @@ namespace QuestPDF.Drawing
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);
@@ -94,11 +110,55 @@ namespace QuestPDF.Drawing
$"1) install the font on your operating system or execution environment. " +
$"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method.");
}
static float GetTextScale(TextStyle style)
{
return style.FontPosition switch
{
FontPosition.Normal => 1f,
FontPosition.Subscript => 0.625f,
FontPosition.Superscript => 0.625f,
_ => throw new ArgumentOutOfRangeException()
};
}
}
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
{
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
}
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());
}
}
}
+1 -1
View File
@@ -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)
{
}
+17 -3
View File
@@ -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
+25 -25
View File
@@ -94,36 +94,36 @@ namespace QuestPDF.Drawing.Proxy
item.Stack.ToList().ForEach(Traverse);
nestingLevel--;
}
static IEnumerable<string> GetElementConfiguration(IElement element)
{
if (element is DebugPointer)
return Enumerable.Empty<string>();
}
internal static IEnumerable<string> GetElementConfiguration(IElement element)
{
if (element is DebugPointer)
return Enumerable.Empty<string>();
return element
.GetType()
.GetProperties()
.Select(x => new
{
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.Select(x => $"{x.Property}: {FormatValue(x.Value)}");
string FormatValue(object value)
return element
.GetType()
.GetProperties()
.Select(x => new
{
const int maxLength = 100;
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.Select(x => $"{x.Property}: {FormatValue(x.Value)}");
string FormatValue(object value)
{
const int maxLength = 100;
var text = value?.ToString() ?? "-";
var text = value?.ToString() ?? "-";
if (text.Length < maxLength)
return text;
if (text.Length < maxLength)
return text;
return text.AsSpan(0, maxLength).ToString() + "...";
}
return text.AsSpan(0, maxLength).ToString() + "...";
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using System.Collections;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class InspectionProxy : ElementProxy
{
public Dictionary<int, InspectionStateItem> Statistics { get; set; } = new();
public InspectionProxy(Element child)
{
Child = child;
}
internal override void Draw(Size availableSpace)
{
if (Canvas is SkiaCanvasBase canvas)
{
var matrix = canvas.Canvas.TotalMatrix;
var inspectionItem = new InspectionStateItem
{
Element = Child,
Position = new Position(matrix.TransX, matrix.TransY),
Size = availableSpace
};
Statistics[PageContext.CurrentPage] = inspectionItem;
}
base.Draw(availableSpace);
}
}
}
@@ -0,0 +1,7 @@
namespace QuestPDF.Drawing.Proxy
{
public class InspectionState
{
}
}
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class InspectionStateItem
{
public Element Element { get; internal set; }
public Size Size { get; internal set; }
public Position Position { get; internal set; }
}
internal class InspectionElementLocation
{
public int PageNumber { get; internal set; }
public float Top { get; set; }
public float Left { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
internal class InspectionElement
{
public string Element { get; internal set; }
public ICollection<InspectionElementLocation> Location { get; internal set; }
public Dictionary<string, string> Properties { get; internal set; }
public ICollection<InspectionElement> Children { get; set; }
}
}
+3 -2
View File
@@ -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)
@@ -1,17 +1,30 @@
using QuestPDF.Drawing;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Previewer
namespace QuestPDF.Drawing
{
record PreviewPage(SKPicture Picture, Size Size);
public class PreviewerPicture
{
public SKPicture Picture { get; set; }
public Size Size { get; set; }
public PreviewerPicture(SKPicture picture, Size size)
{
Picture = picture;
Size = size;
}
}
sealed class PreviewerCanvas : SkiaCanvasBase, IRenderingCanvas
internal class SkiaPictureCanvas : SkiaCanvasBase
{
private SKPictureRecorder? PictureRecorder { get; set; }
private Size? CurrentPageSize { get; set; }
public ICollection<PreviewPage> Pictures { get; } = new List<PreviewPage>();
public ICollection<PreviewerPicture> Pictures { get; } = new List<PreviewerPicture>();
public override void BeginDocument()
{
@@ -22,7 +35,7 @@ namespace QuestPDF.Previewer
{
CurrentPageSize = size;
PictureRecorder = new SKPictureRecorder();
Canvas = PictureRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
}
@@ -31,7 +44,7 @@ namespace QuestPDF.Previewer
var picture = PictureRecorder?.EndRecording();
if (picture != null && CurrentPageSize.HasValue)
Pictures.Add(new PreviewPage(picture, CurrentPageSize.Value));
Pictures.Add(new PreviewerPicture(picture, CurrentPageSize.Value));
PictureRecorder?.Dispose();
PictureRecorder = null;
@@ -39,4 +52,4 @@ namespace QuestPDF.Previewer
public override void EndDocument() { }
}
}
}
+155
View File
@@ -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
};
}
}
}
+16 -2
View File
@@ -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);
}
}
}
}
+106
View File
@@ -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; }
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ namespace QuestPDF.Elements
{
return Children;
}
internal override SpacePlan Measure(Size availableSpace)
{
return Children
+2
View File
@@ -30,6 +30,7 @@ namespace QuestPDF.Elements
public void Compose(IContainer container)
{
container
.Container()
.Background(BackgroundColor)
.Layers(layers =>
{
@@ -40,6 +41,7 @@ namespace QuestPDF.Elements
layers
.PrimaryLayer()
.DebugPointer("Page content")
.MinWidth(MinSize.Width)
.MinHeight(MinSize.Height)
+5 -18
View File
@@ -46,11 +46,6 @@ namespace QuestPDF.Elements
return Items;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Items.ForEach(x => x.Child = create(x.Child));
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!Items.Any())
@@ -102,13 +97,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 +115,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;
+37 -2
View File
@@ -19,15 +19,22 @@ namespace QuestPDF.Elements.Table
private int RowsCount { get; set; }
private int CurrentRow { get; set; }
// cache that stores all cells
// first index: row number
// inner table: list of all cells that ends at the corresponding row
private TableCell[][] CellsCache { get; set; }
private int MaxRow { get; set; }
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
{
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
BuildCache();
base.Initialize(pageContext, canvas);
}
internal override IEnumerable<Element?> GetChildren()
{
return Cells;
@@ -38,6 +45,31 @@ namespace QuestPDF.Elements.Table
Cells.ForEach(x => x.IsRendered = false);
CurrentRow = 1;
}
private void BuildCache()
{
if (CellsCache != null)
return;
if (Cells.Count == 0)
{
MaxRow = 0;
CellsCache = Array.Empty<TableCell[]>();
return;
}
var groups = Cells
.GroupBy(x => x.Row + x.RowSpan - 1)
.ToDictionary(x => x.Key, x => x.OrderBy(x => x.Column).ToArray());
MaxRow = groups.Max(x => x.Key);
CellsCache = Enumerable
.Range(0, MaxRow + 1)
.Select(x => groups.ContainsKey(x) ? groups[x] : Array.Empty<TableCell>())
.ToArray();
}
internal override SpacePlan Measure(Size availableSpace)
{
@@ -143,7 +175,10 @@ namespace QuestPDF.Elements.Table
var rowBottomOffsets = new DynamicDictionary<int, float>();
var commands = new List<TableCellRenderingCommand>();
var cellsToTry = Cells.Where(x => x.Row + x.RowSpan - 1 >= CurrentRow);
var cellsToTry = Enumerable
.Range(CurrentRow, MaxRow - CurrentRow + 1)
.SelectMany(x => CellsCache[x]);
var currentRow = CurrentRow;
var maxRenderingRow = RowsCount;
@@ -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 IsFirstElementInBlock { get; set; }
public bool IsFirstElementInLine { get; set; }
}
}
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Drawing;
namespace QuestPDF.Elements.Text.Calculation
{
@@ -17,6 +18,6 @@ namespace QuestPDF.Elements.Text.Calculation
public int NextIndex { get; set; }
public int TotalIndex { get; set; }
public bool IsLast => NextIndex == EndIndex;
public bool IsLast => EndIndex == TotalIndex;
}
}
@@ -8,6 +8,7 @@ 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)
{
@@ -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; }
+96 -56
View File
@@ -1,8 +1,12 @@
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
@@ -10,9 +14,11 @@ 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 ();
protected virtual bool EnableTextCache => true;
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
{
@@ -23,25 +29,29 @@ namespace QuestPDF.Elements.Text.Items
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 the element is the first one within the line,
// ignore leading spaces
if (request.IsFirstElementInLine)
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
{
while (startIndex < Text.Length && Text[startIndex] == space)
while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint)
startIndex++;
}
if (Text.Length == 0 || startIndex == Text.Length)
if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
{
return new TextMeasurementResult
{
@@ -54,32 +64,19 @@ namespace QuestPDF.Elements.Text.Items
}
// start breaking text from requested position
var text = Text.AsSpan().Slice(startIndex);
var textLength = (int)paint.BreakText(text, request.AvailableWidth + Size.Epsilon);
var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
if (textLength <= 0)
if (endIndex < 0)
return null;
// break text only on spaces
var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine);
var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
if (wrappedTextLength == null)
if (wrappedText == null)
return null;
textLength = wrappedTextLength.Value;
text = text.Slice(0, textLength);
var endIndex = startIndex + textLength;
var nextIndex = endIndex;
// when breaking text, omit spaces at the end of the line
while (nextIndex < Text.Length && Text[nextIndex] == space)
nextIndex++;
// measure final text
var width = paint.MeasureText(text);
var width = TextShapingResult.MeasureWidth(startIndex, wrappedText.Value.endIndex);
return new TextMeasurementResult
{
@@ -91,57 +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)
static int? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
// 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)
{
// textLength - length of the part of the text that fits in available width (creating a line)
// entire text fits, no need to wrap
if (textLength == text.Length)
return textLength;
if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint)
break;
// current line ends at word, next character is space, perfect place to wrap
if (text[textLength - 1] != space && text[textLength] == space)
return textLength;
// find last space within the available text to wrap
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(space);
// text contains space that can be used to wrap
if (lastSpaceIndex > 0)
return lastSpaceIndex;
// there is no available space to wrap text
// if the item is first within the line, perform safe mode and chop the word
// otherwise, move the item into the next line
return isFirstElementInLine ? textLength : null;
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;
}
}
}
+3 -1
View File
@@ -108,7 +108,7 @@ namespace QuestPDF.Elements.Text
.ForEach(x => RenderingQueue.Dequeue());
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.EndIndex;
if (!RenderingQueue.Any())
ResetState();
@@ -170,6 +170,8 @@ namespace QuestPDF.Elements.Text
StartIndex = currentItemIndex,
AvailableWidth = availableWidth - currentWidth,
IsFirstElementInBlock = currentElement == Items.First(),
IsFirstElementInLine = !currentLineElements.Any()
};
+1 -1
View File
@@ -22,7 +22,7 @@ namespace QuestPDF.Fluent
Child = container
});
return container;
return container.DebugPointer("Column Item");;
}
}
+3 -3
View File
@@ -12,7 +12,7 @@ namespace QuestPDF.Fluent
{
var container = new Container();
Decoration.Before = container;
return container;
return container.DebugPointer("Decoration Before");
}
public void Before(Action<IContainer> handler)
@@ -24,7 +24,7 @@ namespace QuestPDF.Fluent
{
var container = new Container();
Decoration.Content = container;
return container;
return container.DebugPointer("Decoration Content");
}
public void Content(Action<IContainer> handler)
@@ -36,7 +36,7 @@ namespace QuestPDF.Fluent
{
var container = new Container();
Decoration.After = container;
return container;
return container.DebugPointer("Decoration After");
}
public void After(Action<IContainer> handler)
@@ -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));
}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ namespace QuestPDF.Fluent
};
Row.Items.Add(element);
return element;
return element.DebugPointer("Row Item");
}
[Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")]
+5 -5
View File
@@ -135,7 +135,7 @@ namespace QuestPDF.Fluent
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
{
var style = DefaultStyle.Clone();
var descriptor = new TextPageNumberDescriptor(DefaultStyle);
var descriptor = new TextPageNumberDescriptor(style);
AddItemToLastTextBlock(new TextBlockPageNumber
{
@@ -174,7 +174,7 @@ namespace QuestPDF.Fluent
public TextPageNumberDescriptor PageNumberWithinSection(string locationName)
{
return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd);
return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageStart);
}
public TextPageNumberDescriptor TotalPagesWithinSection(string locationName)
@@ -185,7 +185,7 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor SectionLink(string? text, string sectionName)
{
if (IsNullOrEmpty(sectionName))
throw new ArgumentException(nameof(sectionName));
throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
@@ -193,7 +193,7 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(text))
return descriptor;
AddItemToLastTextBlock(new TextBlockSectionlLink
AddItemToLastTextBlock(new TextBlockSectionLink
{
Style = style,
Text = text,
@@ -212,7 +212,7 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor Hyperlink(string? text, string url)
{
if (IsNullOrEmpty(url))
throw new ArgumentException(nameof(url));
throw new ArgumentException("Url cannot be null or empty", nameof(url));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
@@ -63,6 +63,12 @@ namespace QuestPDF.Fluent
return descriptor;
}
public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.WrapAnywhere = value;
return descriptor;
}
#region Weight
public static T Weight<T>(this T descriptor, FontWeight weight) where T : TextSpanDescriptor
@@ -120,7 +126,30 @@ namespace QuestPDF.Fluent
{
return descriptor.Weight(FontWeight.ExtraBlack);
}
#endregion
#region Position
public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Normal);
}
public static T Subscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Subscript);
}
public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Superscript);
}
private static T Position<T>(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontPosition = fontPosition;
return descriptor;
}
#endregion
}
}
+31 -1
View File
@@ -71,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
@@ -128,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
}
}
+1 -1
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
namespace QuestPDF.Infrastructure
{
internal enum FontPosition
{
Normal,
Subscript,
Superscript,
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
using QuestPDF.Drawing;
using SkiaSharp;
namespace QuestPDF.Infrastructure
@@ -7,7 +8,7 @@ namespace QuestPDF.Infrastructure
void Translate(Position vector);
void DrawRectangle(Position vector, Size size, string color);
void DrawText(string text, Position position, TextStyle style);
void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style);
void DrawImage(SKImage image, Position position, Size size);
void DrawHyperlink(string url, Size size);
@@ -0,0 +1,35 @@
using System;
using QuestPDF.Elements;
using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure
{
internal class DynamicComponentProxy
{
internal Action<object> SetState { get; private set; }
internal Func<object> GetState { get; private set; }
internal Func<DynamicContext, DynamicComponentComposeResult> Compose { get; private set; }
internal static DynamicComponentProxy CreateFrom<TState>(IDynamicComponent<TState> component) where TState : struct
{
return new DynamicComponentProxy
{
GetState = () => component.State,
SetState = x => component.State = (TState)x,
Compose = component.Compose
};
}
}
public class DynamicComponentComposeResult
{
public IElement Content { get; set; }
public bool HasMoreContent { get; set; }
}
public interface IDynamicComponent<TState> where TState : struct
{
TState State { get; set; }
DynamicComponentComposeResult Compose(DynamicContext context);
}
}
+10 -2
View File
@@ -13,9 +13,11 @@ namespace QuestPDF.Infrastructure
internal float? Size { get; set; }
internal float? LineHeight { get; set; }
internal FontWeight? FontWeight { get; set; }
internal FontPosition? FontPosition { get; set; }
internal bool? IsItalic { get; set; }
internal bool? HasStrikethrough { get; set; }
internal bool? HasUnderline { get; set; }
internal bool? WrapAnywhere { get; set; }
internal object PaintKey { get; private set; }
internal object FontMetricsKey { get; private set; }
@@ -28,9 +30,11 @@ namespace QuestPDF.Infrastructure
Size = 12,
LineHeight = 1.2f,
FontWeight = Infrastructure.FontWeight.Normal,
FontPosition = Infrastructure.FontPosition.Normal,
IsItalic = false,
HasStrikethrough = false,
HasUnderline = false
HasUnderline = false,
WrapAnywhere = false
};
public static TextStyle Default => new TextStyle();
@@ -43,7 +47,7 @@ namespace QuestPDF.Infrastructure
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
PaintKey ??= (FontFamily, Size, FontWeight, IsItalic, Color);
PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic);
}
@@ -55,9 +59,11 @@ namespace QuestPDF.Infrastructure
Size ??= parentStyle.Size;
LineHeight ??= parentStyle.LineHeight;
FontWeight ??= parentStyle.FontWeight;
FontPosition ??= parentStyle.FontPosition;
IsItalic ??= parentStyle.IsItalic;
HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline;
WrapAnywhere ??= parentStyle.WrapAnywhere;
}
internal void OverrideStyle(TextStyle parentStyle)
@@ -68,9 +74,11 @@ namespace QuestPDF.Infrastructure
Size = parentStyle.Size ?? Size;
LineHeight = parentStyle.LineHeight ?? LineHeight;
FontWeight = parentStyle.FontWeight ?? FontWeight;
FontPosition = parentStyle.FontPosition ?? FontPosition;
IsItalic = parentStyle.IsItalic ?? IsItalic;
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
}
internal TextStyle Clone()
+108
View File
@@ -0,0 +1,108 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Previewer
{
public class ExceptionDocument : IDocument
{
private Exception Exception { get; }
public ExceptionDocument(Exception exception)
{
Exception = exception;
}
public DocumentMetadata GetMetadata()
{
return DocumentMetadata.Default;
}
public void Compose(IDocumentContainer document)
{
document.Page(page =>
{
page.ContinuousSize(PageSizes.A4.Width);
page.Margin(50);
page.DefaultTextStyle(x => x.FontSize(16));
page.Foreground().PaddingTop(5).Border(10).BorderColor(Colors.Red.Medium);
page.Header()
.ShowOnce()
.PaddingBottom(5)
.Row(row =>
{
row.Spacing(15);
row.AutoItem()
.PaddingTop(15)
.Width(48)
.AspectRatio(1)
.Canvas((canvas, size) =>
{
const float iconSize = 24;
using var iconPath = SKPath.ParseSvgPathData("M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7");
using var paint = new SKPaint()
{
Color = SKColors.Red,
IsAntialias = true
};
canvas.Scale(size.Width / iconSize);
canvas.DrawPath(iconPath, paint);
});
row.RelativeItem()
.Column(column =>
{
column.Item().Text("Exception").FontSize(36).FontColor(Colors.Red.Medium).Bold();
column.Item().PaddingTop(-10).Text("Don't panic! Just analyze what's happened...").FontSize(18).FontColor(Colors.Red.Medium).Bold();
});
});
page.Content().PaddingVertical(20).Column(column =>
{
var currentException = Exception;
while (currentException != null)
{
column.Item()
.PaddingTop(25)
.PaddingBottom(15)
.Padding(-10)
.Background(Colors.Grey.Lighten3)
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(16));
text.Span(currentException.GetType().Name + ": ").Bold();
text.Span(currentException.Message);
});
foreach (var trace in currentException.StackTrace.Split('\n'))
{
column
.Item()
.ShowEntire()
.BorderBottom(1)
.BorderColor(Colors.Grey.Lighten2)
.PaddingVertical(5)
.Text(trace.Trim())
.FontSize(12);
}
currentException = currentException.InnerException;
}
});
});
}
}
}
@@ -1,4 +1,8 @@
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))]
#if NET6_0_OR_GREATER
using System;
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))]
namespace QuestPDF.Previewer
{
@@ -15,3 +19,5 @@ namespace QuestPDF.Previewer
}
}
}
#endif
+66
View File
@@ -0,0 +1,66 @@
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
public static class Extensions
{
public static void ShowInPreviewer(this IDocument document, int port = 12500)
{
document.ShowInPreviewerAsync(port).ConfigureAwait(true).GetAwaiter().GetResult();
}
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500)
{
var previewerService = new PreviewerService(port);
using var cancellationTokenSource = new CancellationTokenSource();
previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
await previewerService.Connect();
await RefreshPreview();
HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
await WaitForPreviewerExit(cancellationTokenSource.Token);
Task RefreshPreview()
{
var pictures = GetPictures();
return previewerService.RefreshPreview(pictures);
ICollection<PreviewerPicture> GetPictures()
{
try
{
return DocumentGenerator.GeneratePreviewerPictures(document);
}
catch (Exception exception)
{
var exceptionDocument = new ExceptionDocument(exception);
return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument);
}
}
}
async Task WaitForPreviewerExit(CancellationToken cancellationToken)
{
while (true)
{
if (cancellationToken.IsCancellationRequested)
return;
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
}
}
}
#endif
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
#if NET6_0_OR_GREATER
namespace QuestPDF.Previewer
{
internal class PreviewerRefreshCommand
{
public ICollection<Page> Pages { get; set; }
public class Page
{
public string Id { get; } = Guid.NewGuid().ToString("N");
public float Width { get; init; }
public float Height { get; init; }
}
}
}
#endif
+164
View File
@@ -0,0 +1,164 @@
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
namespace QuestPDF.Previewer
{
internal class PreviewerService
{
private int Port { get; }
private HttpClient HttpClient { get; }
public event Action? OnPreviewerStopped;
private const int RequiredPreviewerVersionMajor = 2022;
private const int RequiredPreviewerVersionMinor = 6;
public PreviewerService(int port)
{
Port = port;
HttpClient = new()
{
BaseAddress = new Uri($"http://localhost:{port}/"),
Timeout = TimeSpan.FromSeconds(1)
};
}
public async Task Connect()
{
var isAvailable = await IsPreviewerAvailable();
if (!isAvailable)
{
StartPreviewer();
await WaitForConnection();
}
var previewerVersion = await GetPreviewerVersion();
CheckVersionCompatibility(previewerVersion);
}
private async Task<bool> IsPreviewerAvailable()
{
try
{
using var result = await HttpClient.GetAsync("/ping");
return result.IsSuccessStatusCode;
}
catch
{
return false;
}
}
private async Task<Version> GetPreviewerVersion()
{
using var result = await HttpClient.GetAsync("/version");
return await result.Content.ReadFromJsonAsync<Version>();
}
private void StartPreviewer()
{
try
{
var process = new Process
{
StartInfo = new()
{
FileName = "questpdf-previewer",
Arguments = $"{Port}",
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
Task.Run(async () =>
{
await process.WaitForExitAsync();
process.Dispose();
OnPreviewerStopped?.Invoke();
});
}
catch
{
throw new Exception("Cannot start the QuestPDF Previewer tool. " +
"Please install it by executing in terminal the following command: 'dotnet tool install --global QuestPDF.Previewer'.");
}
}
private void CheckVersionCompatibility(Version version)
{
if (version.Major == RequiredPreviewerVersionMajor && version.Minor == RequiredPreviewerVersionMinor)
return;
throw new Exception($"Previewer version is not compatible. Possible solutions: " +
$"1) Update the QuestPDF library to newer version. " +
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version {RequiredPreviewerVersionMajor}.{RequiredPreviewerVersionMinor}'");
}
private async Task WaitForConnection()
{
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
var cancellationToken = cancellationTokenSource.Token;
while (true)
{
await Task.Delay(TimeSpan.FromMilliseconds(250));
if (cancellationToken.IsCancellationRequested)
throw new Exception($"Cannot connect to the QuestPDF Previewer tool. Please make sure that your Operating System does not block HTTP connections on port {Port}.");
var isConnected = await IsPreviewerAvailable();
if (isConnected)
break;
}
}
public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
{
using var multipartContent = new MultipartFormDataContent();
var pages = new List<PreviewerRefreshCommand.Page>();
foreach (var picture in pictures)
{
var page = new PreviewerRefreshCommand.Page
{
Width = picture.Size.Width,
Height = picture.Size.Height
};
pages.Add(page);
var pictureStream = picture.Picture.Serialize().AsStream();
multipartContent.Add(new StreamContent(pictureStream), page.Id, page.Id);
}
var command = new PreviewerRefreshCommand
{
Pages = pages
};
multipartContent.Add(JsonContent.Create(command), "command");
using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
foreach (var picture in pictures)
picture.Picture.Dispose();
}
}
}
#endif
+12 -5
View File
@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.3.1</Version>
<Version>2022.6.2</Version>
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion>
@@ -18,13 +17,22 @@
<PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Nullable>enable</Nullable>
<TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0;net6.0</TargetFrameworks>
<TargetFramework>net6.0</TargetFramework>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.4" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
<InternalsVisibleTo Include="QuestPDF.ReportSample" />
<InternalsVisibleTo Include="QuestPDF.UnitTests" />
<InternalsVisibleTo Include="QuestPDF.Examples" />
<InternalsVisibleTo Include="QuestPDF.Previewer" />
</ItemGroup>
<ItemGroup>
@@ -37,5 +45,4 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>
+1 -3
View File
@@ -7,9 +7,7 @@
QuestPDF is an open-source .NET library for PDF documents generation.
It offers a layouting engine designed with a full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behavior of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows.
Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in many cases is not reliable. Instead, it implements its own layouting engine that is optimized to cover all paging-related requirements.
It offers a layout engine designed with a full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behavior of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows. All of it done with predictable and discoverable Fluent API.
## Installation
+9 -10
View File
@@ -1,10 +1,9 @@
- Added minimal API to help you quickly start with QuestPDF development and speed up prototyping,
- Improved exception message when desired font type cannot be found (instead of loading default font on Windows and failing with wrong characters on Linux),
- Improved support for custom font types: loading all type faces from a file, respecting true font family, using CSS-like algorithm to find best style match,
- Added support for custom page number formats in the `Text` element, e.g. you can implement roman literal style if required,
- Improved text API to make it more concise,
- Extended support for the `Section` element (previously the `Location` element) by tracking: beginning page number, end page number, page length, page number within location,
- Renaming: the `ExternalLink` element was renamed to the `Hyperlink` element,
- Renaming: the `Location` element was renamed to the `Section` element,
- Renaming: the `InternalLink` element was renamed to the `SectionLink` element,
- Updated homepage and GitHub pages content.
Integrated the text-shaping algorithm. This change significantly improves the Unicode compatibility. Also, it extends support for more advanced languages (e.g. Arabic) that:
1) Combine multiple text characters and display them as a single visual glyph.
2) Are displayed in the right-to-left order.
Improved the exception message when SkiaSharp throws the TypeInitializationException. On some operating systems, SkiaSharp requires additional dependencies installed as nuget packages. This change should help developers determine how to choose and install them correctly.
Fixed: a rare case when the Row.AutoItem() does not correctly calculate the width of its content.
Fixed: the QuestPDF Previewer does not work with content-rich documents.
+1
View File
@@ -0,0 +1 @@
<svg height="180" viewBox="0 0 180 180" width="180" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="32.64" x2="82.77" y1="61.16" y2="85.54"><stop offset=".21" stop-color="#fe2857"/><stop offset="1" stop-color="#293896"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="17.38" x2="82.95" y1="69.86" y2="21.23"><stop offset="0" stop-color="#fe2857"/><stop offset=".01" stop-color="#fe2857"/><stop offset=".86" stop-color="#ff318c"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="74.17" x2="160.27" y1="21.58" y2="99.76"><stop offset=".02" stop-color="#ff318c"/><stop offset=".21" stop-color="#fe2857"/><stop offset=".86" stop-color="#fdb60d"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="155.46" x2="55.07" y1="89.8" y2="158.9"><stop offset=".01" stop-color="#fdb60d"/><stop offset=".86" stop-color="#fcf84a"/></linearGradient><path d="m81.56 83.71-41.35-35a15 15 0 1 0 -14.47 25.7h.15l.39.12 52.16 15.89a3.53 3.53 0 0 0 1.18.21 3.73 3.73 0 0 0 1.93-6.91z" fill="url(#a)"/><path d="m89.85 25.93a10.89 10.89 0 0 0 -16.85-9.18l-50.5 30.66a15 15 0 1 0 17.9 24l45.27-36.89.36-.3a10.93 10.93 0 0 0 3.82-8.29z" fill="url(#b)"/><path d="m163.29 92-76.62-73.79a10.91 10.91 0 1 0 -14.81 16l.14.12 81.4 68.58a7.36 7.36 0 0 0 12.09-5.65 7.39 7.39 0 0 0 -2.2-5.26z" fill="url(#c)"/><path d="m165.5 97.29a7.35 7.35 0 0 0 -11.67-6l-92.71 45.3a15 15 0 1 0 15.48 25.59l85.73-58.84a7.35 7.35 0 0 0 3.17-6.05z" fill="url(#d)"/><path d="m60 60h60v60h-60z"/><g fill="#fff"><path d="m66.53 108.75h22.5v3.75h-22.5z"/><path d="m65.59 75.47 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.45 1.06-1.32v-5.92h2.58v5.94a3.44 3.44 0 0 1 -.92 2.63 3.52 3.52 0 0 1 -2.57 1 3.84 3.84 0 0 1 -3.29-1.62z"/><path d="m73.53 67.52h7.53v2.19h-5v1.43h4.49v2h-4.45v1.49h5v2.2h-7.6z"/><path d="m84.73 69.79h-2.8v-2.27h8.21v2.27h-2.81v7.09h-2.6z"/><path d="m66.63 80.58h4.42a3.47 3.47 0 0 1 2.55.83 2.09 2.09 0 0 1 .61 1.52 2.18 2.18 0 0 1 -1.45 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.69-1.31 2.69-3.55 2.69h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.25c.78 0 1.24-.27 1.24-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86-.02-.53-.4-.87-1.27-.87z"/><path d="m75.45 80.58h4.15a4.14 4.14 0 0 1 3.05 1 2.92 2.92 0 0 1 .83 2.18 3 3 0 0 1 -1.93 2.89l2.24 3.35h-3l-1.89-2.84h-.87v2.84h-2.6zm4 4.5c.87 0 1.4-.43 1.4-1.12 0-.75-.55-1.13-1.41-1.13h-1.39v2.27z"/><path d="m87.09 80.51h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.74h-2.71zm2.28 5.73-1.05-2.65-1.06 2.65z"/><path d="m94 80.55h2.6v9.37h-2.6z"/><path d="m97.56 80.55h2.44l3.37 5v-5h2.57v9.37h-2.27l-3.53-5.14v5.14h-2.58z"/><path d="m106.37 88.53 1.44-1.73a4.86 4.86 0 0 0 3 1.13c.71 0 1.08-.25 1.08-.65 0-.41-.3-.61-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.38-3 3.63-3a5.88 5.88 0 0 1 3.85 1.25l-1.25 1.78a4.56 4.56 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .43.32.62 1.63.91 2.15.47 3.48 1.17 3.48 2.92 0 1.91-1.51 3-3.78 3a6.56 6.56 0 0 1 -4.4-1.45z"/></g><path d="m0 0h180v180h-180z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+7
View File
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "6.0.300",
"rollForward": "latestMinor",
"allowPrerelease": true
}
}
+32 -5
View File
@@ -37,7 +37,13 @@ As an open-source project without funding, I cannot afford advertising QuestPDF
It doesn't matter if you are a professional developer, creating a startup or work for an established company. All of us care about our tools and dependencies, about stability and security, about time and money we can safe, about quality we can offer. Please consider sponsoring QuestPDF to give me an extra motivational push to develop the next great feature.
> If you represent a company, want to help the entire community and show that you care, please consider sponsoring QuestPDF using one of the higher tiers. Your company logo will be shown here for all developers, building a strong positive relation.
> If you represent a company and want to help the entire community, please consider sponsoring QuestPDF using one of the higher tiers. All developers will see your company logo and the description of your choice. It is a fantastic way to build a strong relationship with the community, show that you care, or even find the best professionals. The truth is, no classical advertisement campaign is as effective as real engagement.
Special thanks to all companies that decided to sponsor QuestPDF development. This makes .NET ecosystem a better place for all developers and businesses!
| Company | Description |
|--------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------|
| <img src="Resources/jetbrains-logo.svg" width="100px"> | [JetBrains](https://www.jetbrains.com/) supports this project as part of the OSS Power-Ups program. Thank you!<br/>100$ / month |
[![Sponsor project](https://img.shields.io/badge/%E2%9D%A4%EF%B8%8F%20sponsor-QuestPDF-red)](https://github.com/sponsors/QuestPDF)
@@ -53,7 +59,7 @@ Install-Package QuestPDF
dotnet add package QuestPDF
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.3.1" />
<PackageReference Include="QuestPDF" Version="2022.6.0" />
```
[![Nuget version](https://img.shields.io/badge/package%20details-QuestPDF-blue?logo=nuget)](https://www.nuget.org/packages/QuestPDF/)
@@ -68,11 +74,20 @@ A short and easy to follow tutorial showing how to design an invoice document un
A detailed description of behavior of all available components and how to use them with C# Fluent API.
[![Patterns and Practices](https://img.shields.io/badge/%F0%9F%94%8D%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)
[![Patterns and Practices](https://img.shields.io/badge/%E2%9C%A8%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)
Everything that may help you designing great reports and create reusable code that is easy to maintain.
## QuestPDF Previewer
<!-- 🚀 📖 ️-->
The QuestPDF Previewer is a tool designed to simplify and speed up your development lifecycle. First, it shows a preview of your document. But the real magic starts with the hot-reload capability! It observes your code and updates the preview every time you change the implementation. Get real-time results without the need of code recompilation. Save time and enjoy the task!
[![Learn more](https://img.shields.io/badge/%F0%9F%93%96%20Previewer-learn%20more-blue)](https://www.questpdf.com/documentation/document-previewer.html)
<video src="https://github.com/QuestPDF/QuestPDF-Documentation/blob/a6f54912ee761af14dfbe1f96aa70d7fcf7ff94f/images/previewer/video.mp4?raw=true"></video>
<img src="https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/previewer/animation.gif" width="100%">
## Simplicity is the key
@@ -90,7 +105,7 @@ Document.Create(container =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Background(Colors.White);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
@@ -128,3 +143,15 @@ And compare it to the produced PDF file:
The Fluent API of QuestPDF scales really well. It is easy to create and maintain even most complex documents. Read [the Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html) to learn QuestPDF basics and implement an invoice under 200 lines of code. You can also investigate and play with the code from [the example repository](https://github.com/QuestPDF/example-invoice).
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/invoice.png" width="400px">
## QuestPDF on JetBrains OSS Power-Ups
QuestPDF was presented on one of the episodes of OSS Power-Ups hosted by JetBrains. Huge thanks for Matthias Koch and entire JetBrains team for giving me a chance to show QuestPDF. You are the best!
<a href="https://www.youtube.com/watch?v=-iYvZvpLX0g">
<img src="https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/jetbrains-oss-powerups-youtube.png" width="600px">
</a>
[![YouTube video about QuestPDF]( https://img.shields.io/badge/watch%20on-YouTube-red?logo=youtube)](https://www.youtube.com/watch?v=-iYvZvpLX0g)