diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 98d31ab..ccf2e35 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -19,14 +19,21 @@ jobs: with: dotnet-version: 10.0.x + # The release tag drives the published version; _build/Version.props is the fallback default. + - name: Resolve version from release tag + id: version + run: echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + env: + TAG: ${{ github.event.release.tag_name }} + - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore + run: dotnet build --no-restore -c Release -p:Version=${{ steps.version.outputs.version }} - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet test --no-build -c Release --verbosity normal - name: pack - run: dotnet pack -c Release PdfSharpDslCore/PdfSharpDslCore.csproj + run: dotnet pack --no-build -c Release PdfSharpDslCore/PdfSharpDslCore.csproj -p:Version=${{ steps.version.outputs.version }} # - name: add publish # run: dotnet nuget add source --username pgourlain --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/pgourlain/index.json" # - name: publish package diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2c8ad..6ba94a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,21 @@ # Change log -## Version 1.0.6 (September 20, 2024) +## Version 2.0.0 (unreleased) +* Migrated PDF generation from PdfSharpCore and its image/font/archive dependencies to TerraPDF 2.2.0. +* Added engine-independent drawing primitives and `PdfSharpDsl.Language` for the netstandard2.0 source generator. +* Added `PublishPdf(Stream)`, `PublishPdf(string)` and `PublishPdf()` to `PdfDocumentDrawer`. +* **Breaking:** `PdfSharpDslCore` now multi-targets `net8.0` and `net10.0` (was `netstandard2.0`). +* Centralized the build in a `_build/` folder: `Version.props` holds the single product version, `Common.props` the shared package metadata, licence and target-framework aliases, and `Packages.props` every NuGet version (central package management). `Directory.Build.props`, `Directory.Build.targets` and `Directory.Packages.props` at the repository root are thin stubs that import them. +* All assemblies now ship the same version. `PdfSharpDslCore.Generator` moves from `1.0.2` to `2.0.0` and `PdfSharpDslConsole` no longer carries its own `0.1.0`. +* The release workflow now derives the published package version from the GitHub release tag, and builds, tests and packs in `Release` so the tested binaries are the ones packed. +* Updated `Microsoft.Extensions.Logging.Abstractions` and `Microsoft.Extensions.Logging.Console` to 10.0.12. + +## Version 1.0.6 (September 20, 2026) * Migrated console and test projects to .NET 10 while retaining reusable projects on .NET Standard 2.0. * Added reproducible core-only coverage enforcement at 90% line coverage. * Added Roslyn source-generator compilation and clean NuGet consumer validation. - ## Version 1.0.5 (March 3, 2024) * Update nugets packages and upgrade to .Net 8 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..157c6e2 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..cddace9 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,6 @@ + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..922f068 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/PdfSharpDslCore/Drawing/DebugOptions.cs b/PdfSharpDsl.Language/Drawing/DebugOptions.cs similarity index 100% rename from PdfSharpDslCore/Drawing/DebugOptions.cs rename to PdfSharpDsl.Language/Drawing/DebugOptions.cs diff --git a/PdfSharpDsl.Language/Drawing/DrawingPrimitives.cs b/PdfSharpDsl.Language/Drawing/DrawingPrimitives.cs new file mode 100644 index 0000000..a57ec16 --- /dev/null +++ b/PdfSharpDsl.Language/Drawing/DrawingPrimitives.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PdfSharpDslCore.Drawing +{ + public readonly struct PdfPoint + { + public PdfPoint(double x, double y) + { + X = x; + Y = y; + } + + public double X { get; } + public double Y { get; } + + public PdfPoint OffsetY(double offsetY) => new(X, Y + offsetY); + } + + public readonly struct PdfSize + { + public PdfSize(double width, double height) + { + Width = width; + Height = height; + } + + public double Width { get; } + public double Height { get; } + } + + public struct PdfRect + { + public PdfRect(double x, double y, double width, double height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + public double X { readonly get; private set; } + public double Y { readonly get; private set; } + public double Width { readonly get; private set; } + public double Height { readonly get; private set; } + + public static PdfRect Empty => new(0, 0, -1, -1); + + public PdfRect(PdfPoint first, PdfPoint second) + : this(Math.Min(first.X, second.X), Math.Min(first.Y, second.Y), + Math.Abs(second.X - first.X), Math.Abs(second.Y - first.Y)) + { + } + + public PdfRect(PdfPoint location, PdfSize size) + : this(location.X, location.Y, size.Width, size.Height) + { + } + + public readonly bool IsEmpty => Width < 0 || Height < 0; + public readonly double Left => X; + public readonly double Top => Y; + public readonly double Right => X + Width; + public readonly double Bottom => Y + Height; + public readonly PdfPoint TopLeft => new(X, Y); + + public void Offset(double offsetX, double offsetY) + { + X += offsetX; + Y += offsetY; + } + + public readonly PdfRect OffsetY(double offsetY) => new(X, Y + offsetY, Width, Height); + + public void Union(PdfPoint point) + { + Union(new PdfRect(point.X, point.Y, 0, 0)); + } + + public void Union(PdfRect rect) + { + if (rect.IsEmpty) + { + return; + } + + if (IsEmpty) + { + this = rect; + return; + } + + var left = Math.Min(Left, rect.Left); + var top = Math.Min(Top, rect.Top); + var right = Math.Max(Right, rect.Right); + var bottom = Math.Max(Bottom, rect.Bottom); + this = new PdfRect(left, top, right - left, bottom - top); + } + + public void Intersect(PdfRect rect) + { + var left = Math.Max(Left, rect.Left); + var top = Math.Max(Top, rect.Top); + var right = Math.Min(Right, rect.Right); + var bottom = Math.Min(Bottom, rect.Bottom); + this = right < left || bottom < top + ? Empty + : new PdfRect(left, top, right - left, bottom - top); + } + } + + public readonly struct PdfColor + { + public PdfColor(byte alpha, byte red, byte green, byte blue) + { + Alpha = alpha; + Red = red; + Green = green; + Blue = blue; + } + + public byte Alpha { get; } + public byte Red { get; } + public byte Green { get; } + public byte Blue { get; } + + public byte A => Alpha; + + public static PdfColor FromArgb(byte alpha, byte red, byte green, byte blue) => + new(alpha, red, green, blue); + + public static PdfColor FromRgb(byte red, byte green, byte blue) => new(255, red, green, blue); + + public static PdfColor FromArgb(uint argb) => new( + (byte)(argb >> 24), + (byte)(argb >> 16), + (byte)(argb >> 8), + (byte)argb); + + public static PdfColor FromGrayScale(double value) + { + var component = (byte)Math.Min(255, Math.Max(0, (int)Math.Round(value * 255))); + return FromRgb(component, component, component); + } + + public static PdfColor Black => FromRgb(0, 0, 0); + public static PdfColor White => FromRgb(255, 255, 255); + public static PdfColor RedColor => FromRgb(255, 0, 0); + public static PdfColor Transparent => new(0, 0, 0, 0); + + internal string Hex => $"#{Red:X2}{Green:X2}{Blue:X2}"; + internal double Opacity => Alpha / 255d; + } + + public static class PdfColors + { + private static readonly IReadOnlyDictionary Colors = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["black"] = PdfColor.FromRgb(0, 0, 0), + ["blue"] = PdfColor.FromRgb(0, 0, 255), + ["crimson"] = PdfColor.FromRgb(220, 20, 60), + ["darkblue"] = PdfColor.FromRgb(0, 0, 139), + ["darkgreen"] = PdfColor.FromRgb(0, 100, 0), + ["darkslategray"] = PdfColor.FromRgb(47, 79, 79), + ["gold"] = PdfColor.FromRgb(255, 215, 0), + ["gray"] = PdfColor.FromRgb(128, 128, 128), + ["green"] = PdfColor.FromRgb(0, 128, 0), + ["lightblue"] = PdfColor.FromRgb(173, 216, 230), + ["lightgray"] = PdfColor.FromRgb(211, 211, 211), + ["lightgreen"] = PdfColor.FromRgb(144, 238, 144), + ["lightsalmon"] = PdfColor.FromRgb(255, 160, 122), + ["lightseagreen"] = PdfColor.FromRgb(32, 178, 170), + ["maroon"] = PdfColor.FromRgb(128, 0, 0), + ["orange"] = PdfColor.FromRgb(255, 165, 0), + ["purple"] = PdfColor.FromRgb(128, 0, 128), + ["red"] = PdfColor.FromRgb(255, 0, 0), + ["slategray"] = PdfColor.FromRgb(112, 128, 144), + ["steelblue"] = PdfColor.FromRgb(70, 130, 180), + ["tomato"] = PdfColor.FromRgb(255, 99, 71), + ["transparent"] = PdfColor.Transparent, + ["violet"] = PdfColor.FromRgb(238, 130, 238), + ["white"] = PdfColor.FromRgb(255, 255, 255), + ["yellow"] = PdfColor.FromRgb(255, 255, 0), + }; + + public static IEnumerable Names => Colors.Keys; + + public static PdfColor FromName(string name) + { + return Colors.TryGetValue(name, out var color) ? color : PdfColor.Black; + } + } + + public enum PdfDashStyle + { + Solid, + Dash, + Dot, + DashDot, + DashDotDot, + } + + public sealed class PdfPen + { + public PdfPen(PdfColor color, double width) + { + Color = color; + Width = width; + } + + public PdfColor Color { get; } + public double Width { get; } + public PdfDashStyle DashStyle { get; set; } = PdfDashStyle.Solid; + } + + public sealed class PdfBrush + { + public PdfBrush(PdfColor color) + { + Color = color; + } + + public PdfColor Color { get; } + } + + [Flags] + public enum PdfFontStyle + { + Regular = 0, + Bold = 1, + Italic = 2, + BoldItalic = Bold | Italic, + Underline = 4, + Strikeout = 8, + } + + public sealed class PdfFont + { + public PdfFont(string familyName, double size, PdfFontStyle style = PdfFontStyle.Regular) + { + FamilyName = familyName; + Size = size; + Style = style; + } + + public string FamilyName { get; } + public double Size { get; } + public PdfFontStyle Style { get; } + } + + public sealed class PdfImage + { + private readonly byte[] _data; + + public PdfImage(ReadOnlySpan data) + { + _data = data.ToArray(); + } + + public ReadOnlyMemory Data => _data; + } + + public sealed class PdfMargins + { + private double _all; + + public double All + { + set => Left = Top = Right = Bottom = _all = value; + get => _all; + } + + public double Left { get; set; } + public double Top { get; set; } + public double Right { get; set; } + public double Bottom { get; set; } + } + + public enum PdfHorizontalAlignment + { + Near, + Center, + Far, + } + + public enum PdfVerticalAlignment + { + Near, + Center, + Far, + } + + public enum PdfPageOrientation + { + Portrait, + Landscape, + } + + public enum PdfPageSize + { + Undefined, + A0, + A1, + A2, + A3, + A4, + A5, + A6, + Letter, + Legal, + Ledger, + Tabloid, + } +} \ No newline at end of file diff --git a/PdfSharpDslCore/Evaluation/BinaryEvaluation.cs b/PdfSharpDsl.Language/Evaluation/BinaryEvaluation.cs similarity index 100% rename from PdfSharpDslCore/Evaluation/BinaryEvaluation.cs rename to PdfSharpDsl.Language/Evaluation/BinaryEvaluation.cs diff --git a/PdfSharpDslCore/Evaluation/BinaryOperation.cs b/PdfSharpDsl.Language/Evaluation/BinaryOperation.cs similarity index 100% rename from PdfSharpDslCore/Evaluation/BinaryOperation.cs rename to PdfSharpDsl.Language/Evaluation/BinaryOperation.cs diff --git a/PdfSharpDslCore/Evaluation/ConstantEvaluation.cs b/PdfSharpDsl.Language/Evaluation/ConstantEvaluation.cs similarity index 100% rename from PdfSharpDslCore/Evaluation/ConstantEvaluation.cs rename to PdfSharpDsl.Language/Evaluation/ConstantEvaluation.cs diff --git a/PdfSharpDslCore/Evaluation/CustomFunctionEvaluation.cs b/PdfSharpDsl.Language/Evaluation/CustomFunctionEvaluation.cs similarity index 100% rename from PdfSharpDslCore/Evaluation/CustomFunctionEvaluation.cs rename to PdfSharpDsl.Language/Evaluation/CustomFunctionEvaluation.cs diff --git a/PdfSharpDslCore/Evaluation/Evaluation.cs b/PdfSharpDsl.Language/Evaluation/Evaluation.cs similarity index 100% rename from PdfSharpDslCore/Evaluation/Evaluation.cs rename to PdfSharpDsl.Language/Evaluation/Evaluation.cs diff --git a/PdfSharpDslCore/Evaluation/Evaluator.cs b/PdfSharpDsl.Language/Evaluation/Evaluator.cs similarity index 100% rename from PdfSharpDslCore/Evaluation/Evaluator.cs rename to PdfSharpDsl.Language/Evaluation/Evaluator.cs diff --git a/PdfSharpDslCore/Evaluation/UnaryEvaluation.cs b/PdfSharpDsl.Language/Evaluation/UnaryEvaluation.cs similarity index 100% rename from PdfSharpDslCore/Evaluation/UnaryEvaluation.cs rename to PdfSharpDsl.Language/Evaluation/UnaryEvaluation.cs diff --git a/PdfSharpDslCore/Evaluation/VariableEvaluation.cs b/PdfSharpDsl.Language/Evaluation/VariableEvaluation.cs similarity index 100% rename from PdfSharpDslCore/Evaluation/VariableEvaluation.cs rename to PdfSharpDsl.Language/Evaluation/VariableEvaluation.cs diff --git a/PdfSharpDsl.Language/Extensions/ParseTreeNodeExtensions.cs b/PdfSharpDsl.Language/Extensions/ParseTreeNodeExtensions.cs new file mode 100644 index 0000000..90fdb21 --- /dev/null +++ b/PdfSharpDsl.Language/Extensions/ParseTreeNodeExtensions.cs @@ -0,0 +1,97 @@ +using Irony.Parsing; +using PdfSharpDslCore.Drawing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +[assembly:InternalsVisibleTo("PdfSharpDslCore.Generator")] + +namespace PdfSharpDslCore.Extensions +{ + internal static class ParseTreeNodeExtensions + { + public static IEnumerable ChildNodes(this ParseTreeNode node, string termName) + { + List result = new List(); + Queue queue = new Queue(); + + queue.Enqueue(node); + + while (queue.Count > 0) + { + var n = queue.Dequeue(); + if (n.Term != null && n.Term.Name == termName) + { + result.Add(n); + } + foreach (var item in n.ChildNodes) + { + queue.Enqueue(item); + } + } + return result.AsReadOnly(); + } + + public static ParseTreeNode? ChildNode(this ParseTreeNode node, string termName) + { + return node.ChildNodes.FirstOrDefault(n => n.Term != null && n.Term.Name == termName); + } + + public static PdfFontStyle ParseFontStyle(this ParseTreeNode? node) + { + if (node != null && node.Token != null) + { + var styleName = (string?)node.Token.Value; + if (Enum.TryParse(styleName, true, out var fontStyle)) + { + return fontStyle; + } + } + return PdfFontStyle.Regular; + } + + public static PdfColor ParseColor(this ParseTreeNode node) + { + var executor = (Func)(node.ChildNodes[0].Term.Name switch + { + "NamedColor" => ParseNamedColor, + _ => ParseHexColor, + }); + + return executor(node.ChildNodes[0]); + } + private static PdfColor ParseNamedColor(ParseTreeNode node) + { + var color = (string)node.ChildNodes[0].Token.Value; + return PdfColors.FromName(color); + } + + private static PdfColor ParseHexColor(ParseTreeNode node) + { + var colorValue = node.ChildNodes[0].Token.Value; + if (colorValue is double) + { + return PdfColor.FromGrayScale(Convert.ToDouble(colorValue)); + } + else + { + if (node.ChildNodes[0].Token.Length == 8) + { + uint argb = ((uint)0xff000000) | Convert.ToUInt32(colorValue); + return PdfColor.FromArgb(argb); + } + else if (node.ChildNodes[0].Token.Length == 10) + { + uint argb = unchecked((uint)Convert.ToInt32(colorValue)); + return PdfColor.FromArgb(argb); + } + return PdfColor.FromArgb(unchecked((uint)Convert.ToInt32(colorValue))); + } + } + + } +} diff --git a/PdfSharpDsl.Language/Parser/PdfGrammar.cs b/PdfSharpDsl.Language/Parser/PdfGrammar.cs new file mode 100644 index 0000000..4ee099f --- /dev/null +++ b/PdfSharpDsl.Language/Parser/PdfGrammar.cs @@ -0,0 +1,502 @@ + +using Irony; +using Irony.Parsing; +using PdfSharpDslCore.Drawing; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; + + +[assembly: InternalsVisibleTo("pdfsharpdslTests")] + + +namespace PdfSharpDslCore.Parser +{ + [Language("PdfGrammar", "1.0", "Grammar to write PDF with PdfSharp")] + public class PdfGrammar : Grammar + { + protected NonTerminal FormulaRoot; + private NonTerminal EmbbededSmtListOpt; + + public PdfGrammar() + { + var sstring = new StringLiteral("string", "\"", StringOptions.AllowsDoubledQuote | StringOptions.AllowsAllEscapes) + { + Priority = TerminalPriority.High + }; + var textString = new StringLiteral("textstring", "\"", StringOptions.AllowsDoubledQuote | StringOptions.AllowsAllEscapes | StringOptions.AllowsLineBreak); + var number_literal = new NumberLiteral("number", NumberOptions.AllowSign); + var pixel_literal = new NumberLiteral("pixel", NumberOptions.IntOnly) + { + DefaultFloatType = TypeCode.Double, + DefaultIntTypes = new TypeCode[3] + { + TypeCode.Int32, + TypeCode.Int64, + (TypeCode)30 + } + }; + pixel_literal.AddSuffix("px", TypeCode.Int64); + + var colorNumber = new NumberLiteral("ColorValue"); + colorNumber.AddPrefix("g", NumberOptions.Default | NumberOptions.AllowStartEndDot); + colorNumber.AddPrefix("0x", NumberOptions.Hex); + var variableLiteral = new IdentifierTerminal("var"); + KeyTerm lpar = ToTerm("("); + KeyTerm rpar = ToTerm(")"); + KeyTerm comma = ToTerm(",", "comma"); + + var comment = new CommentTerminal("comment", "#", "\r", "\n", "\u2085", "\u2028", "\u2029"); + //comment must to be added to NonGrammarTerminals list; it is not used directly in grammar rules, + // so we add it to this list to let Scanner know that it is also a valid terminal. + NonGrammarTerminals.Add(comment); + + #region variables + var PDF = new NonTerminal("PDF"); + var PdfLine = new NonTerminal("PdfLine"); + var PdfInstruction = new NonTerminal("PdfInstruction"); + var PdfPrimaryInstruction = new NonTerminal("PdfPrimaryInstruction"); + var SetSmt = new NonTerminal("SetSmt"); + var RectSmt = new NonTerminal("RectSmt"); + var LineSmt = new NonTerminal("LineSmt"); + var EllipseSmt = new NonTerminal("EllipseSmt"); + var LineToSmt = new NonTerminal("LineToSmt"); + var MoveToSmt = new NonTerminal("MoveToSmt"); + var FillRectSmt = new NonTerminal("FillRectSmt"); + var FillEllipseSmt = new NonTerminal("FillEllipseSmt"); + var PenSmt = new NonTerminal("PenSmt"); + var BrushSmt = new NonTerminal("BrushSmt"); + var HBrushSmt = new NonTerminal("HBrushSmt"); + var BrushType = new NonTerminal("BrushType"); + var FontSmt = new NonTerminal("FontSmt"); + var ImageSmt = new NonTerminal("ImageSmt"); + + var ColorExp = new NonTerminal("ColorExp"); + var NamedColor = new NonTerminal("NamedColor"); + var HexColor = new NonTerminal("HexColor"); + var styleExpr = new NonTerminal("styleExpr"); + var RectLocation = new NonTerminal("RectLocation"); + var PointLocation = new NonTerminal("PointLocation"); + var SetContent = new NonTerminal("SetContent"); + var TextSmt = new NonTerminal("TextSmt"); + var RectOrPointLocation = new NonTerminal("RectOrPointLocation"); + var TextAlignment = new NonTerminal("TextAlignment"); + var TextOrientation = new NonTerminal("TextOrientation"); + var TextOrientationValue = new NonTerminal("TextOrientationValue"); + var HAlign = new NonTerminal("HAlign"); + var VAlign = new NonTerminal("VAlign"); + var VAlignValue = new NonTerminal("VAlignValue"); + var HAlignValue = new NonTerminal("HAlignValue"); + var LineTextSmt = new NonTerminal("LineTextSmt"); + var NewPageSmt = new NonTerminal("NewPage"); + var PageSize = new NonTerminal("PageSize"); + var PageOrientation = new NonTerminal("PageOrientation"); + var TableSmt = new NonTerminal("TableSmt"); + var TableContent = new NonTerminal("TableContent"); + var TableHead = new NonTerminal("TableHead"); + var TableRowList = new NonTerminal("TableRowList"); + var TableColList = new NonTerminal("TableColList"); + var TableRow = new NonTerminal("TableRow"); + var TableCol = new NonTerminal("TableCol"); + var TableLocation = new NonTerminal("TableLocation"); + var TableHeadStyle = new NonTerminal("TableHeadStyle"); + var TableColHeadList = new NonTerminal("TableColHeadList"); + var TableHeadCol = new NonTerminal("TableHeadCol"); + var TableColWidth = new NonTerminal("TableColWidth"); + var TableColFont = new NonTerminal("TableColFont"); + var TableColColors = new NonTerminal("TableColColors"); + var TableRowStyle = new NonTerminal("TableRowStyle"); + var TableRowListOrRowTemplate = new NonTerminal("TableRowListOrRowTemplate"); + var TableRowTemplate = new NonTerminal("TableRowTemplate"); + var TableRowTemplateCount = new NonTerminal("TableRowTemplateCount"); + var TableCellColSpan = new NonTerminal("TableCellColSpan"); + var TableCellRowSpan = new NonTerminal("TableCellRowSpan"); + + var PointAutoLocation = new NonTerminal("PointAutoLocation"); + var NumberOrAuto = new NonTerminal("NumberOrAuto"); + var ViewSizeSmt = new NonTerminal("ViewSizeSmt"); + var TitleSmt = new NonTerminal("TitleSmt"); + var MarginArg = new NonTerminal("MarginArg"); + var Parenthesized_Expression = new NonTerminal("Parenthesized_Expression"); + var BinaryExpression = new NonTerminal("BinaryExpression"); + var BinOp = new NonTerminal("BinOp", "operator"); + var UnOp = new NonTerminal("UnOp"); + var semiOpt = new NonTerminal("semiOpt"); + var PixelOrPoint = new NonTerminal("PixelOrPoint"); + var CropExp = new NonTerminal("CropExp"); + var ImageLocation = new NonTerminal("ImageLocation"); + var PieSmt = new NonTerminal("PieSmt"); + var FillPieSmt = new NonTerminal("FillPieSmt"); + var PolygonSmt = new NonTerminal("PolygonSmt"); + var PolygonPoint = new NonTerminal("PolygonPoint"); + var FillPolygonSmt = new NonTerminal("FillPolygonSmt"); + var ForSmt = new NonTerminal("ForSmt"); + var UdfSmt = new NonTerminal("UdfSmt"); + var UdfInvokeSmt = new NonTerminal("UdfInvokeSmt"); + var IfSmt = new NonTerminal("IfSmt"); + var Else_clause_opt = new NonTerminal("Else_clause_opt"); + var then_clause = new NonTerminal("then_clause"); + + var FormulaExpression = new NonTerminal("FormulaExpression"); + var LiteralExpression = new NonTerminal("LiteralExpression"); + var FormulaPrimary = new NonTerminal("FormulaTerm"); + var UnaryExpression = new NonTerminal("UnaryExpression"); + var VarSmt = new NonTerminal("VarSmt"); + var VarRef = new NonTerminal("VarRef"); + + var RowTemplateSmt = new NonTerminal("RowTemplateSmt"); + var DebugOptionsSmt = new NonTerminal("DebugOptionsSmt"); + #endregion + + #region Formula rules + RegisterOperators(20, "and", "or"); + RegisterOperators(30, "==", "<=", ">=", "<", ">", "<>"); + RegisterOperators(40, "+", "-"); + RegisterOperators(50, "*", "/", "%"); + RegisterOperators(60, "^"); + + FormulaRoot = FormulaExpression; + var CustomFunctionExpression = new NonTerminal("CustomFunctionExpression"); + FormulaExpression.Rule = BinaryExpression | FormulaPrimary; + FormulaPrimary.Rule = LiteralExpression | UnaryExpression | Parenthesized_Expression | CustomFunctionExpression; + + LiteralExpression.Rule = number_literal | VarRef | sstring; + UnaryExpression.Rule = UnOp + FormulaExpression; + Parenthesized_Expression.Rule = lpar + FormulaExpression + rpar; + BinaryExpression.Rule = FormulaExpression + BinOp + FormulaExpression; + + var CallInvokeArgumentslist = new NonTerminal("CallInvokeArgumentslist"); + CallInvokeArgumentslist.Rule = MakePlusRule(CallInvokeArgumentslist, comma, FormulaExpression); + + var CustomFunctionArgs = new NonTerminal("CustomFunctionArgs"); + CustomFunctionExpression.Rule = variableLiteral + CustomFunctionArgs; + var CustomFunctionArgsOpt = new NonTerminal("CustomFunctionArgsOpt"); + CustomFunctionArgs.Rule = lpar + CustomFunctionArgsOpt + rpar; + CustomFunctionArgsOpt.Rule = Empty | CallInvokeArgumentslist; + + UnOp.Rule = ToTerm("+") | "-"; + VarRef.Rule = "$" + variableLiteral; + BinOp.Rule = ToTerm("+") | "-" | "*" | "/" | "%" | "==" | "<=" | ">=" | "<" | ">" | "<>" | "and" | "or"; + MarkTransient(FormulaExpression, LiteralExpression, BinOp, FormulaPrimary, + Parenthesized_Expression, UnOp); + #endregion + + // set the PROGRAM to be the root node of PDF lines. + Root = PDF; + + // BNF Rules + PDF.Rule = MakeStarRule(PDF, PdfLine); + + // A line can be an empty line, or it's a number followed by a statement list ended by a new-line. + KeyTerm semi = ToTerm(";", "semi"); + semi.ErrorAlias = "';' expected"; + + comma.ErrorAlias = "',' expected"; + semiOpt.Rule = Empty | semi; + + PdfLine.Rule = UdfSmt | DebugOptionsSmt | PdfInstruction; + + PdfInstruction.Rule = PdfPrimaryInstruction + semiOpt; + + PdfPrimaryInstruction.Rule = SetSmt + | RectSmt + | FillRectSmt + | EllipseSmt + | FillEllipseSmt + | TitleSmt + | NewPageSmt + | ViewSizeSmt + | LineSmt + | LineToSmt + | MoveToSmt + | LineTextSmt + | TextSmt + | TableSmt + | ImageSmt + | PieSmt + | PolygonSmt + | FillPolygonSmt + | FillPieSmt + | ForSmt + | UdfInvokeSmt + | IfSmt + | RowTemplateSmt + ; + + #region basics rules + RectLocation.Rule = PointLocation + comma + PointLocation; + + PointLocation.Rule = FormulaExpression + comma + FormulaExpression; + RectOrPointLocation.Rule = RectLocation | PointLocation; + + #endregion + + SetSmt.Rule = ToInstructionTerm("SET") + SetContent; + SetContent.Rule = PenSmt | BrushSmt | FontSmt | VarSmt | HBrushSmt; + RectSmt.Rule = ToInstructionTerm("RECT") + RectLocation; + FillRectSmt.Rule = ToInstructionTerm("FILLRECT") + RectLocation; + EllipseSmt.Rule = ToInstructionTerm("ELLIPSE") + RectLocation; + FillEllipseSmt.Rule = ToInstructionTerm("FILLELLIPSE") + RectLocation; + + + LineSmt.Rule = ToInstructionTerm("LINE") + RectLocation; + MoveToSmt.Rule = ToInstructionTerm("MOVETO") + PointLocation; + LineToSmt.Rule = ToInstructionTerm("LINETO") + PointLocation; + var stylePenOpt = new NonTerminal("StylePenOpt"); + var stylePen = new NonTerminal("StylePen"); + stylePenOpt.Rule = Empty | stylePen; + stylePen.Rule = ToTerm("solid") | "dash" | "dot" | "dashdot" | "dashdotdot"; + PenSmt.Rule = ToTerm("PEN") + ColorExp + FormulaExpression + stylePenOpt; + BrushSmt.Rule = ToTerm("BRUSH") + ColorExp + BrushType; + //TODO: how to deactivate HBRUSH... + HBrushSmt.Rule = ToTerm("HBRUSH") + ColorExp + BrushType; + //do not use ToTerm, because "FONT" is used in as argument in Table + FontSmt.Rule = new KeyTerm("FONT", "FONT") + Arg("Name") + FormulaExpression + Arg("Size") + FormulaExpression + styleExpr; + + VarSmt.Rule = ToTerm("VAR") + variableLiteral + "=" + FormulaExpression + semi; + + ColorExp.Rule = NamedColor | HexColor; + foreach (var colorName in PdfColors.Names) + { + var name = colorName.ToLowerInvariant(); + if (NamedColor.Rule == null) + { + NamedColor.Rule = ToTerm(name, $"color-{name}"); + } + else + { + NamedColor.Rule |= ToTerm(name, $"color-{name}"); + } + } + + + HexColor.Rule = colorNumber; + styleExpr.Rule = Empty; + foreach (var enumName in Enum.GetNames(typeof(PdfFontStyle))) + { + var styleName = enumName.ToLowerInvariant(); + styleExpr.Rule |= ToTerm(styleName, $"style-{styleName}"); + } + + + //Multiline alignment is implemented by the TerraPDF canvas adapter. + //multiline + TextSmt.Rule = ToInstructionTerm("TEXT") + RectOrPointLocation + OptArg("MaxWidth", FormulaExpression) + Arg("Text") + FormulaExpression; + + TextAlignment.Rule = HAlign + VAlign; + HAlign.Rule = Empty | Arg("HAlign") + HAlignValue; + VAlign.Rule = Empty | Arg("VAlign") + VAlignValue; + HAlignValue.Rule = ToTerm("left") | "right" | "hcenter"; + VAlignValue.Rule = ToTerm("top") | "bottom" | "vcenter"; + + TextOrientation.Rule = Empty | Arg("Orientation") + TextOrientationValue; + TextOrientationValue.Rule = FormulaExpression + | "vertical" + | "horizontal"; + + //simple line + LineTextSmt.Rule = ToInstructionTerm("LINETEXT") + RectOrPointLocation + TextAlignment + TextOrientation + + Arg("Text") + FormulaExpression; + BrushType.Rule = Empty /* | GradientBrush*/; + + PageSize.Rule = Empty; + + var names = Enum.GetNames(typeof(PdfPageSize)); + var firstSize = names.First(); + PageSize.Rule |= ToTerm(firstSize, $"pagesize-{firstSize}"); + foreach (var prop in names.Skip(1)) + { + PageSize.Rule |= ToTerm(prop, $"pagesize-{prop}"); + } + + PageOrientation.Rule = Empty | "portrait" | "landscape"; + NewPageSmt.Rule = ToInstructionTerm("NEWPAGE") + PageSize + PageOrientation; + + TitleSmt.Rule = ToInstructionTerm("TITLE") + MarginArg + HAlign + Arg("Text") + FormulaExpression; + MarginArg.Rule = Empty | Arg("Margin") + FormulaExpression; + + + TableSmt.Rule = ToInstructionTerm("TABLE") + TableLocation + TableContent + ToTerm("ENDTABLE"); + + TableContent.Rule = TableHead + TableRowListOrRowTemplate; + + TableHead.Rule = ToTerm("HEAD") + TableHeadStyle + TableColHeadList + ToTerm("ENDHEAD"); + TableRowList.Rule = MakeStarRule(TableRowList, TableRow); + TableRowListOrRowTemplate.Rule = TableRowList | TableRowTemplate; + TableRowTemplateCount.Rule = FormulaExpression; + TableRowTemplate.Rule = ToInstructionTerm("ROWTEMPLATE") + TableRowTemplateCount + TableColList + ToTerm("ENDROW"); + TableColHeadList.Rule = MakeStarRule(TableColHeadList, TableHeadCol); + TableHeadCol.Rule = ToTerm("COL") + TableColWidth + TableColFont + TableColColors + sstring + semi; + //desiredWidth and maxWidth + TableColWidth.Rule = Arg("Width") + NumberOrAuto + Arg("MaxWidth") + NumberOrAuto; + TableColList.Rule = MakeStarRule(TableColList, TableCol); + TableRow.Rule = ToTerm("ROW") + TableRowStyle + TableColList + ToTerm("ENDROW"); + TableCellColSpan.Rule = Empty | Arg("ColSpan") + number_literal; + TableCellRowSpan.Rule = Empty | Arg("RowSpan") + number_literal; + TableCol.Rule = ToTerm("COL") + TableCellColSpan + TableCellRowSpan + TextAlignment + FormulaExpression + semi; + TableLocation.Rule = PointLocation /*+ "," + PointAutoLocation*/; + PointAutoLocation.Rule = NumberOrAuto + "," + NumberOrAuto; + NumberOrAuto.Rule = FormulaExpression | "auto"; + TableHeadStyle.Rule = Empty | ColorExp; + TableColFont.Rule = Empty | Arg("FONT") + sstring + "," + number_literal + "," + styleExpr; + TableColColors.Rule = Empty | ColorExp + ColorExp; + TableRowStyle.Rule = Empty | FormulaExpression; + + ViewSizeSmt.Rule = ToInstructionTerm("VIEWSIZE") + PointLocation; + + PixelOrPoint.Rule = ToTerm("pixel") | "point"; + CropExp.Rule = Empty | "crop" | "fit"; + ImageLocation.Rule = PointLocation | RectLocation + PixelOrPoint + CropExp; + //PreferShift because + var ImageRawOrSource = new NonTerminal("ImageRawOrSource"); + ImageSmt.Rule = ToInstructionTerm("IMAGE") + ImageLocation + ImageRawOrSource + FormulaExpression; + ImageRawOrSource.Rule = Arg("Source") | Arg("Data"); + + PieSmt.Rule = ToInstructionTerm("PIE") + RectLocation + Arg("Start") + FormulaExpression + Arg("Angle") + FormulaExpression; + PolygonSmt.Rule = ToInstructionTerm("POLYGON") + PointLocation + comma + PointLocation + comma + PolygonPoint; + PolygonPoint.Rule = MakePlusRule(PolygonPoint, comma, PointLocation); + FillPieSmt.Rule = ToInstructionTerm("FILLPIE") + RectLocation + Arg("Start") + FormulaExpression + Arg("Angle") + FormulaExpression; + FillPolygonSmt.Rule = ToInstructionTerm("FILLPOLYGON") + PointLocation + comma + PointLocation + comma + PolygonPoint; + + var EmbbededSmtList = new NonTerminal("EmbbededSmtList"); + var ForBlock = new NonTerminal("ForBlock"); + ForSmt.Rule = ToInstructionTerm("FOR") + variableLiteral + "=" + FormulaExpression + "TO" + FormulaExpression + ForBlock; + var embbededSmtListOpt = new NonTerminal("EmbbededSmtListOpt"); + this.EmbbededSmtListOpt = embbededSmtListOpt; + ForBlock.Rule = ToTerm("DO") + embbededSmtListOpt + "ENDFOR"; + embbededSmtListOpt.Rule = Empty + EmbbededSmtList; + EmbbededSmtList.Rule = MakePlusRule(EmbbededSmtList, null, PdfInstruction); + + var UdfArgumentslistOpt = new NonTerminal("UdfArgumentslistOpt"); + var UdfArguments = new NonTerminal("UdfArguments"); + var UdfArgumentslist = new NonTerminal("UdfArgumentslist"); + var UdfBlock = new NonTerminal("UdfBlock"); + UdfArguments.Rule = lpar + UdfArgumentslistOpt + rpar; + UdfArgumentslistOpt.Rule = Empty | UdfArgumentslist; + UdfSmt.Rule = ToTerm("UDF") + variableLiteral + PreferShiftHere() + UdfArguments + UdfBlock; + UdfArgumentslist.Rule = MakePlusRule(UdfArgumentslist, comma, variableLiteral); + UdfBlock.Rule = embbededSmtListOpt + "ENDUDF"; + + + var UdfInvokeArguments = new NonTerminal("UdfInvokeArguments"); + var UdfInvokeArgumentslistOpt = new NonTerminal("UdfInvokeArgumentslistOpt"); + UdfInvokeSmt.Rule = ToInstructionTerm("CALL") + variableLiteral + PreferShiftHere() + UdfInvokeArguments; + UdfInvokeArguments.Rule = lpar + UdfInvokeArgumentslistOpt + rpar; + UdfInvokeArgumentslistOpt.Rule = Empty | CallInvokeArgumentslist; + + IfSmt.Rule = ToInstructionTerm("IF") + FormulaExpression + then_clause + Else_clause_opt + "ENDIF"; + then_clause.Rule = "THEN" + embbededSmtListOpt; + Else_clause_opt.Rule = Empty | PreferShiftHere() + "ELSE" + embbededSmtListOpt; + + ConfigureRowTemplateSmt(RowTemplateSmt); + + var debugOption = new IdentifierTerminal("debugOption"); + var debugOptionList = new NonTerminal("debugOptionList"); + debugOptionList.Rule = MakePlusRule(debugOptionList, comma, debugOption); + DebugOptionsSmt.Rule = "DEBUGOPTIONS" + debugOptionList + semi; + + + RegisterBracePair("(", ")"); + + MarkPunctuation(";", ",", "(", ")", "TABLE", "ENDTABLE", "HEAD", "ENDHEAD", "ROW", "ROWTEMPLATE ", "ENDROW", "ENDFOR", "UDF", "ENDUDF", + "IF", "THEN", "ELSE", "ENDIF", "ROWTEMPLATE", "ENDROWTEMPLATE"); + RegisterBracePair("(", ")"); + MarkTransient(PdfLine, PdfPrimaryInstruction, SetContent, NumberOrAuto, + styleExpr, semiOpt, PixelOrPoint, HAlignValue, TextOrientationValue, VAlignValue, + embbededSmtListOpt, + UdfArguments, UdfArgumentslistOpt, + UdfInvokeArguments, UdfInvokeArgumentslistOpt, stylePenOpt, + CustomFunctionArgs, CustomFunctionArgsOpt, TableRowTemplateCount); + + this.AddTermsReportGroup("punctuation", comma); + this.AddToNoReportGroup("(", "++", "--"); + this.AddOperatorReportGroup("operator"); + this.AddTermsReportGroup("constant", number_literal, sstring); + this.AddTermsReportGroup("constant", "auto"); + this.AddToNoReportGroup(semi); + } + + void ConfigureRowTemplateSmt(NonTerminal rowTemplateSmt) + { + /* + ROWTEMPLATE Count=getGlobalCommentsCount() + LINETEXT 50, 100 HAlign=center VAlign=bottom Text=getGlobalCommentDate($ROWINDEX); + + SET VAR GINDEX=$ROWINDEX; + ROWTEMPLATE Count=getCommentsCount($GINDEX) + LINETEXT 150, 100 HAlign=center VAlign=bottom Text=getCommentDate($GINDEX,$ROWINDEX); + ENDROWTEMPLATE + ENDROWTEMPLATE + */ + var rowTemplateContent = new NonTerminal("RowTemplateBlock"); + rowTemplateSmt.Rule = "ROWTEMPLATE" + Arg("Count") + FormulaRoot + Arg("Y") + FormulaRoot + + OptArg("Name", FormulaRoot) + + OptArg("BorderSize", FormulaRoot) + + OptArg("NewPageTopMargin", FormulaRoot) + //+ OptArg("Splitable", FormulaRoot) + + rowTemplateContent; + rowTemplateContent.Rule = EmbbededSmtListOpt + "ENDROWTEMPLATE"; + } + + KeyTerm ToInstructionTerm(string name) + { + var result = ToTerm(name); + AddTermsReportGroup("instruction", result); + return result; + } + + /// + /// for argument in grammar + /// + /// + /// + private BnfExpression Arg(string name) + { + var term = ToTerm(name); + term.ErrorAlias = $"Missing argument '{name}=...'"; + var result = term + PreferShiftHere() + "="; + //result.ErrorAlias = $"argument missing {name}"; + return result; + } + + BnfExpression OptArg(string name, BnfExpression bnfExpression) + { + var term = new NonTerminal($"Opt-{name}") + { + Rule = Empty | name + PreferShiftHere() + "=" + bnfExpression + }; + return term; + } + + public override string ConstructParserErrorMessage(ParsingContext context, StringSet expectedTerms) + { + if (context.CurrentParserState.ExpectedTerminals.Count > 0 && expectedTerms.Count == 0) + { + expectedTerms.AddRange(TerminalToString(context.CurrentParserState.ExpectedTerminals)); + return base.ConstructParserErrorMessage(context, expectedTerms); + } + return base.ConstructParserErrorMessage(context, expectedTerms); + } + + private string[] TerminalToString(TerminalSet expectedTerminals) + { + var l = new List(); + foreach (var item in expectedTerminals) + { + l.Add(item switch { + KeyTerm k => k.Text, + _ => item.ToString(), + }); + } + return l.ToArray(); + } + + public override void ReportParseError(ParsingContext context) + { + base.ReportParseError(context); + } + } +} \ No newline at end of file diff --git a/PdfSharpDslCore/Parser/PdfParserException.cs b/PdfSharpDsl.Language/Parser/PdfParserException.cs similarity index 100% rename from PdfSharpDslCore/Parser/PdfParserException.cs rename to PdfSharpDsl.Language/Parser/PdfParserException.cs diff --git a/PdfSharpDslCore/Parser/PdfVisitor.cs b/PdfSharpDsl.Language/Parser/PdfVisitor.cs similarity index 100% rename from PdfSharpDslCore/Parser/PdfVisitor.cs rename to PdfSharpDsl.Language/Parser/PdfVisitor.cs diff --git a/PdfSharpDsl.Language/PdfSharpDsl.Language.csproj b/PdfSharpDsl.Language/PdfSharpDsl.Language.csproj new file mode 100644 index 0000000..b13dffc --- /dev/null +++ b/PdfSharpDsl.Language/PdfSharpDsl.Language.csproj @@ -0,0 +1,19 @@ + + + + $(NetStandardTfm) + false + + + + + + + + + + + + + + diff --git a/PdfSharpDslConsole/Fonts/MyFontResolver.cs b/PdfSharpDslConsole/Fonts/MyFontResolver.cs deleted file mode 100644 index 1405442..0000000 --- a/PdfSharpDslConsole/Fonts/MyFontResolver.cs +++ /dev/null @@ -1,43 +0,0 @@ -using PdfSharpCore.Fonts; -using PdfSharpCore.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace PdfSharpDslConsole.Fonts -{ - internal class MyFontResolver : IFontResolver - { - private readonly IFontResolver _fontResolver; - Dictionary fontmap = new Dictionary(); - public string DefaultFontName => "Arial"; - public MyFontResolver(IEnumerable fontFiles) - { - _fontResolver = new FontResolver(); - if (fontFiles != null && fontFiles.Any()) - { - foreach (var fontFile in fontFiles) - { - fontmap.Add(Path.GetFileName(fontFile).ToLowerInvariant(), fontFile); - } - FontResolver.SetupFontsFiles(fontFiles.ToArray()); - } - } - public byte[] GetFont(string faceName) - { - if (fontmap.TryGetValue(faceName.ToLowerInvariant(), out var file)) - { - return File.ReadAllBytes(file); - } - - return _fontResolver.GetFont(faceName); - } - - public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic) - { - return _fontResolver.ResolveTypeface(familyName, isBold, isItalic); - } - } -} diff --git a/PdfSharpDslConsole/PdfSharpDslConsole.csproj b/PdfSharpDslConsole/PdfSharpDslConsole.csproj index 621402a..6849a12 100644 --- a/PdfSharpDslConsole/PdfSharpDslConsole.csproj +++ b/PdfSharpDslConsole/PdfSharpDslConsole.csproj @@ -2,11 +2,9 @@ Exe - net10.0 + $(NetAppTfm) enable - enable - 0.1.0 - + false true $(MSBuildThisFileDirectory)\Generated @@ -14,7 +12,7 @@ - + @@ -57,6 +55,9 @@ PreserveNewest + + PreserveNewest + diff --git a/PdfSharpDslConsole/Program.cs b/PdfSharpDslConsole/Program.cs index 55a1491..bbdcfec 100644 --- a/PdfSharpDslConsole/Program.cs +++ b/PdfSharpDslConsole/Program.cs @@ -1,13 +1,9 @@ using System.Globalization; -using PdfSharpCore.Fonts; -using PdfSharpCore.Pdf; -using PdfSharpDslConsole.Fonts; using PdfSharpDslCore.Drawing; using PdfSharpDslCore.Parser; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Logging.Console; +using TerraPDF.Helpers; ServiceProvider serviceProvider = new ServiceCollection() .AddLogging((loggingBuilder) => loggingBuilder @@ -27,10 +23,6 @@ logger?.LogDebug("Debug World"); logger?.LogInformation("Hello World"); -//GlobalFontSettings.FontResolver = new FontResolver(); -GlobalFontSettings.DefaultFontEncoding = PdfFontEncoding.Unicode; - - #region global variables var globalComments = new[] { @@ -95,11 +87,12 @@ //var fileName = "pdfsharp-newpage.ipdf"; //var fileName = "pdfsharp.ipdf"; //var fileName = "sample1.ipdf"; -var fileName = "demo.ipdf"; -if (args.Length > 0) -{ - fileName = args[0]; -} +var fileName = args.Length > 0 ? args[0] : "demo.ipdf"; +var outputFile = Path.GetFullPath("helloworld.pdf"); +if (args.Length == 0) + Directory.SetCurrentDirectory(AppContext.BaseDirectory); +if (!Path.IsPathRooted(fileName) && !File.Exists(fileName)) + fileName = Path.Combine(AppContext.BaseDirectory, fileName); var parsingResult = parser.Parse(File.ReadAllText(fileName)); @@ -115,11 +108,14 @@ } else { - GlobalFontSettings.FontResolver = new MyFontResolver(LocalFontFiles()); - //PdfSharpCore cclasses - var document = new PdfDocument(); + RegisterSystemFonts(); + foreach (var font in LocalFontNames().Zip(LocalFontFiles())) + { + FontFamily.Register(font.First, font.Second); + } + //draw parsing result - using var drawer = new PdfDocumentDrawer(document, logger); + using var drawer = new PdfDocumentDrawer(logger); var visitor = new PdfDrawerVisitor(logger); visitor.RegisterFormulaFunction("GetFontCount", (_) => LocalFontNames().Count()); @@ -132,7 +128,8 @@ visitor.RegisterFormulaFunction("GETCOMMENTAUTHOR", getCommentAuthor); visitor.Draw(drawer, parsingResult); - document.Save("helloworld.pdf"); + drawer.PublishPdf(outputFile); + Console.WriteLine($"PDF generated: {outputFile}"); //var a = new PDfDsl.pdfsharp(); //a.WritePdf(drawer); @@ -140,12 +137,12 @@ IEnumerable LocalFontFiles() { - yield return @"Fonts/AlexBrush-Regular.ttf"; - yield return @"Fonts/Just-Signature.ttf"; - yield return @"Fonts/Inspiration-Regular.ttf"; - yield return @"Fonts/Quirlycues.ttf"; - yield return @"Fonts/Rabiohead.ttf"; - yield return @"Fonts/SCRIPTIN.ttf"; + yield return Path.Combine(AppContext.BaseDirectory, "Fonts", "AlexBrush-Regular.ttf"); + yield return Path.Combine(AppContext.BaseDirectory, "Fonts", "Just-Signature.ttf"); + yield return Path.Combine(AppContext.BaseDirectory, "Fonts", "Inspiration-Regular.ttf"); + yield return Path.Combine(AppContext.BaseDirectory, "Fonts", "Quirlycues.ttf"); + yield return Path.Combine(AppContext.BaseDirectory, "Fonts", "Rabiohead.ttf"); + yield return Path.Combine(AppContext.BaseDirectory, "Fonts", "SCRIPTIN.ttf"); } IEnumerable LocalFontNames() @@ -158,6 +155,26 @@ IEnumerable LocalFontNames() yield return "Scriptina"; } +void RegisterSystemFonts() +{ + var fontsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); + RegisterFontVariant("Arial", "arial.ttf", fontsDirectory); + RegisterFontVariant("Arial", "arialbd.ttf", fontsDirectory, bold: true); + RegisterFontVariant("Arial", "ariali.ttf", fontsDirectory, italic: true); + RegisterFontVariant("Arial", "arialbi.ttf", fontsDirectory, bold: true, italic: true); + RegisterFontVariant("Consolas", "consola.ttf", fontsDirectory); + RegisterFontVariant("Consolas", "consolab.ttf", fontsDirectory, bold: true); + RegisterFontVariant("Consolas", "consolai.ttf", fontsDirectory, italic: true); + RegisterFontVariant("Consolas", "consolaz.ttf", fontsDirectory, bold: true, italic: true); +} + +void RegisterFontVariant(string familyName, string fileName, string directory, bool bold = false, bool italic = false) +{ + var path = Path.Combine(directory, fileName); + if (File.Exists(path)) + FontFamily.Register(familyName, path, bold, italic); +} + object GetFontNameByIndex(object[] arguments) { var index = (int)arguments[0]; diff --git a/PdfSharpDslConsole/helloworld-with-pdfsharp.pdf b/PdfSharpDslConsole/helloworld-with-pdfsharp.pdf new file mode 100644 index 0000000..01f4ce0 Binary files /dev/null and b/PdfSharpDslConsole/helloworld-with-pdfsharp.pdf differ diff --git a/PdfSharpDslConsole/helloworld.pdf b/PdfSharpDslConsole/helloworld.pdf index 8044ef3..1f4a2fa 100644 Binary files a/PdfSharpDslConsole/helloworld.pdf and b/PdfSharpDslConsole/helloworld.pdf differ diff --git a/PdfSharpDslCore.Generator/DrawingGenerator/CSharpDrawer.cs b/PdfSharpDslCore.Generator/DrawingGenerator/CSharpDrawer.cs deleted file mode 100644 index 0ebfd60..0000000 --- a/PdfSharpDslCore.Generator/DrawingGenerator/CSharpDrawer.cs +++ /dev/null @@ -1,186 +0,0 @@ -using PdfSharpCore; -using PdfSharpCore.Drawing; -using PdfSharpDslCore.Drawing; -using System; -using System.Collections.Generic; -using System.Text; - -namespace PdfSharpDslCore.Generator.DrawingGenerator -{ - internal class CSharpDrawer : IPdfDocumentDrawer - { - StringBuilder _code; - string _drawerPrefix; - int vNameIndex = 1; - public CSharpDrawer(StringBuilder code, string drawerPrefix) - { - _code = code; - _drawerPrefix = drawerPrefix; - } - public XPen CurrentPen - { - get => throw new NotImplementedException("CurrentPen"); - set - { - var penvName = $"pen{vNameIndex++}"; - _code.AppendLine($"var {penvName} = new XPen({ColorToString(value.Color)}, {value.Width});"); - _code.AppendLine($"{penvName}.DashStyle = XDashStyle.{value.DashStyle};"); - _code.AppendLine($"{_drawerPrefix}CurrentPen = {penvName};"); - } - } - - uint Argb(XColor color) - { - var _a = color.A; - var _r = color.R; - var _g = color.G; - var _b = color.B; - return ((uint)(_a * 255) << 24) | ((uint)_r << 16) | ((uint)_g << 8) | _b; - } - - private string ColorToString(XColor color) - { - if (color.IsKnownColor) - { - return $"XColors.{XColorResourceManager.GetKnownColor(Argb(color))}"; - } - else - { - return $"XColor.FromArgb({color.A},{color.R},{color.G},{color.B})"; - } - } - - public XBrush CurrentBrush - { - get => throw new NotImplementedException("CurrentBrush"); - set { } - } - - public XBrush HighlightBrush - { - get => throw new NotImplementedException(); - set { } - } - public XFont CurrentFont - { - get => throw new NotImplementedException("CurrentFont"); - set { } - } - - public double PageWidth => 100;//throw new NotImplementedException(); - - public double PageHeight => 100;//throw new NotImplementedException(); - public DebugOptions DebugOptions { get; set; } - - public void DrawEllipse(double x, double y, double w, double h, bool isFilled) - { - //throw new NotImplementedException(); - } - - public void DrawImage(XImage image, double x, double y, double? w, double? h, bool sizeInPixel, bool cropImage) - { - //throw new NotImplementedException(); - } - - public void DrawLine(double x, double y, double x1, double y1) - { - //throw new NotImplementedException(); - } - - public void DrawLineText(string text, double x, double y, double? w, double? h, XStringAlignment hAlign, XLineAlignment vAlign, TextOrientation textOrientation) - { - //throw new NotImplementedException("DrawLineText"); - } - - public void DrawPie(double x, double y, double? w, double? h, double startAngle, double sweepAngle, bool isFilled) - { - //throw new NotImplementedException(); - } - - public void DrawPolygon(IEnumerable points, bool isFilled) - { - //throw new NotImplementedException(); - } - - public void DrawRect(double x, double y, double w, double h, bool isFilled) - { - //throw new NotImplementedException(); - } - - public void DrawTable(double x, double y, TableDefinition tblDef) - { - //throw new NotImplementedException(); - } - - public void DrawText(string text, double x, double y, double? w, double? h) - { - //throw new NotImplementedException(); - } - - public void DrawTitle(string text, double margin, XStringAlignment hAlign, XLineAlignment vAlign) - { - //throw new NotImplementedException(); - } - - public void LineTo(double x, double y) - { - //throw new NotImplementedException(); - } - - public void MoveTo(double x, double y) - { - //throw new NotImplementedException(); - } - - public void NewPage(PageSize? pageSize = null, PageOrientation? pageOrientation = null) - { - //throw new NotImplementedException(); - } - - public void SetViewSize(double w, double h) - { - //throw new NotImplementedException(); - } - - public void BeginDrawRowTemplate(string name, int index, double offsetY, double newPageTopMargin) - { - //throw new NotImplementedException(); - } - - public DrawingResult EndDrawRowTemplate(int index) - { - //throw new NotImplementedException(); - return new DrawingResult(); - } - - public void BeginIterationTemplate(int rowCount) - { - //throw new NotImplementedException(); - } - - public void EndIterationTemplate(double drawHeight) - { - //throw new NotImplementedException(); - } - - public void RegisterOnNewPage(Action callback) - { - //throw new NotImplementedException(); - } - - public void UnRegisterOnNewPage(Action callback) - { - //throw new NotImplementedException(); - } - - public void SetOffsetY(double offsetY) - { - throw new NotImplementedException(); - } - - public void ResetOffset() - { - throw new NotImplementedException(); - } - } -} diff --git a/PdfSharpDslCore.Generator/DrawingGenerator/CSharpGeneratorState.cs b/PdfSharpDslCore.Generator/DrawingGenerator/CSharpGeneratorState.cs index 08ef97a..7680b5d 100644 --- a/PdfSharpDslCore.Generator/DrawingGenerator/CSharpGeneratorState.cs +++ b/PdfSharpDslCore.Generator/DrawingGenerator/CSharpGeneratorState.cs @@ -61,7 +61,6 @@ private StringBuilder ConstructCode() #nullable enable namespace PDfDsl { using System.Collections.Generic; - using PdfSharpCore.Drawing; using PdfSharpDslCore.Drawing; //another comments "); diff --git a/PdfSharpDslCore.Generator/DrawingGenerator/CSharpVisitor.cs b/PdfSharpDslCore.Generator/DrawingGenerator/CSharpVisitor.cs index e0c07d8..591fa44 100644 --- a/PdfSharpDslCore.Generator/DrawingGenerator/CSharpVisitor.cs +++ b/PdfSharpDslCore.Generator/DrawingGenerator/CSharpVisitor.cs @@ -1,6 +1,4 @@ using Irony.Parsing; -using PdfSharpCore; -using PdfSharpCore.Drawing; using PdfSharpDslCore.Drawing; using PdfSharpDslCore.Evaluation; using PdfSharpDslCore.Extensions; @@ -65,11 +63,10 @@ protected override void ExecutePen(IGeneratorState state, ParseTreeNode widthNod ParseTreeNode styleNode) { var penvName = $"pen{vNameIndex++}"; - state.AppendLine($"var {penvName} = new XPen({ColorToString(colorNode.ParseColor())}, (double){EvaluateForString(widthNode).StringValue});"); - //_code.AppendLine($"var {penvName} = new XPen({ColorToString(value.Color)}, {value.Width});"); - if (styleNode != null && Enum.TryParse< XDashStyle>(styleNode.Token.ValueString, true, out var penStyle)) + state.AppendLine($"var {penvName} = new PdfPen({ColorToString(colorNode.ParseColor())}, (double){EvaluateForString(widthNode).StringValue});"); + if (styleNode != null && Enum.TryParse(styleNode.Token.ValueString, true, out var penStyle)) { - state.AppendLine($"{penvName}.DashStyle = XDashStyle.{penStyle.ToString()};"); + state.AppendLine($"{penvName}.DashStyle = PdfDashStyle.{penStyle};"); } state.AppendLine($"{_prefix}CurrentPen = {penvName};"); } @@ -104,25 +101,9 @@ private EvaluationResult EvaluateForString(ParseTreeNode node) return new CSharpEvaluator(_prefix, node).EvaluateForCSharpString(_declaredVariables, _declaredFunctions); } - uint Argb(XColor color) + private static string ColorToString(PdfColor color) { - var _a = color.A; - var _r = color.R; - var _g = color.G; - var _b = color.B; - return ((uint)(_a * 255) << 24) | ((uint)_r << 16) | ((uint)_g << 8) | _b; - } - - private string ColorToString(XColor color) - { - if (color.IsKnownColor) - { - return $"XColors.{XColorResourceManager.GetKnownColor(Argb(color))}"; - } - else - { - return $"XColor.FromArgb({color.A},{color.R},{color.G},{color.B})"; - } + return $"PdfColor.FromArgb({color.Alpha}, {color.Red}, {color.Green}, {color.Blue})"; } } } \ No newline at end of file diff --git a/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj b/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj index 31c8db4..8eee82a 100644 --- a/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj +++ b/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj @@ -1,54 +1,46 @@ - + - netstandard2.0 + $(NetStandardTfm) true - 9.0 + + 8.0 + + disable Pdf Generation using source generation - 1.0.6 - Pierrick Gourlain - - https://github.com/pgourlain/bnf_and_pdf - LICENSE.md false false + + false true true + true - - - - - - - - - - - - + + + + - + - - - - - + + + + @@ -58,11 +50,7 @@ - - - - - + diff --git a/PdfSharpDslCore/Drawing/DrawingContext.cs b/PdfSharpDslCore/Drawing/DrawingContext.cs index 70294c0..21e6caa 100644 --- a/PdfSharpDslCore/Drawing/DrawingContext.cs +++ b/PdfSharpDslCore/Drawing/DrawingContext.cs @@ -1,5 +1,4 @@ -using PdfSharpCore.Drawing; -using System; +using System; using System.Collections.Generic; using Irony; using Microsoft.Extensions.Logging; @@ -9,8 +8,8 @@ namespace PdfSharpDslCore.Drawing internal class DrawingContext { private readonly BlocksRecorder _recorder; - private readonly Stack _previousGraphics = new(); - public int Level => _previousGraphics.Count; + private int _level; + public int Level => _level; public DebugOptions DebugOptions { get; set; } public bool DebugText => (DebugOptions & (DebugOptions.DebugText | DebugOptions.DebugAll)) > 0; @@ -21,18 +20,18 @@ public DrawingContext(ILogger? logger) _recorder = new(logger); } - public void OpenBlock(string name, double offsetY, XGraphics previousGraphics, double newPageTopMargin) + public void OpenBlock(string name, double offsetY, double newPageTopMargin) { - _previousGraphics.Push(previousGraphics); + _level++; _recorder.OpenBlock(name, offsetY, true, newPageTopMargin); } - public XRect BlockRect => _recorder.CurrentBlock.Rect; - internal (IInstructionBlock, XGraphics) RestoreGraphics() + public PdfRect BlockRect => _recorder.CurrentBlock.Rect; + internal IInstructionBlock EndMeasure() { var block = _recorder.CurrentBlock; - - return (block, _previousGraphics.Pop()); + _level--; + return block; } internal void CloseBlock() @@ -40,7 +39,7 @@ internal void CloseBlock() _recorder.CloseBlock(); } - public void PushInstruction(Action action, XRect rect, bool accumulate=true, string instrName="") + public void PushInstruction(Action action, PdfRect rect, bool accumulate=true, string instrName="") { if (_recorder.CanPushInstruction) { @@ -49,9 +48,9 @@ public void PushInstruction(Action action, XRect rect, bool accumulate=t } } - public void PushInstruction(Action action, XPoint[] ptArray) + public void PushInstruction(Action action, PdfPoint[] ptArray) { - var r = XRect.Empty; + var r = PdfRect.Empty; foreach (var pt in ptArray) { r.Union(pt); diff --git a/PdfSharpDslCore/Drawing/DrawingHelper.cs b/PdfSharpDslCore/Drawing/DrawingHelper.cs index 0956954..d0e1004 100644 --- a/PdfSharpDslCore/Drawing/DrawingHelper.cs +++ b/PdfSharpDslCore/Drawing/DrawingHelper.cs @@ -1,7 +1,4 @@ -using PdfSharpCore.Drawing; -using PdfSharpCore.Pdf; - -namespace PdfSharpDslCore.Drawing +namespace PdfSharpDslCore.Drawing { internal static class DrawingHelper { @@ -12,30 +9,31 @@ internal static class DrawingHelper /// /// /// - public static XRect RectFromStringFormat(XRect r, XSize textSize, XStringFormat fmt) + public static PdfRect RectFromStringFormat(PdfRect r, PdfSize textSize, + PdfHorizontalAlignment horizontalAlignment, PdfVerticalAlignment verticalAlignment) { - var result = new XRect(r.TopLeft, textSize); + var result = new PdfRect(r.TopLeft, textSize); - switch (fmt.Alignment) + switch (horizontalAlignment) { - case XStringAlignment.Center: + case PdfHorizontalAlignment.Center: result.Offset((r.Width - textSize.Width) / 2, 0); break; - case XStringAlignment.Near: + case PdfHorizontalAlignment.Near: break; - case XStringAlignment.Far: + case PdfHorizontalAlignment.Far: result.Offset(r.Width - textSize.Width, 0); break; } - switch (fmt.LineAlignment) + switch (verticalAlignment) { - case XLineAlignment.Center: + case PdfVerticalAlignment.Center: result.Offset(0, (r.Height - textSize.Height) / 2); break; - case XLineAlignment.Near: + case PdfVerticalAlignment.Near: break; - case XLineAlignment.Far: + case PdfVerticalAlignment.Far: result.Offset(0, r.Height - textSize.Height); break; } @@ -45,32 +43,33 @@ public static XRect RectFromStringFormat(XRect r, XSize textSize, XStringFormat return result; } - public static XRect RectFromStringFormat(double x, double y, XSize textSize, XStringFormat fmt) + public static PdfRect RectFromStringFormat(double x, double y, PdfSize textSize, + PdfHorizontalAlignment horizontalAlignment, PdfVerticalAlignment verticalAlignment) { - var result = new XRect(x, y, textSize.Width, textSize.Height); + var result = new PdfRect(x, y, textSize.Width, textSize.Height); var xOffset = 0.0; var yOffset = 0.0; - switch (fmt.Alignment) + switch (horizontalAlignment) { - case XStringAlignment.Center: + case PdfHorizontalAlignment.Center: xOffset -= textSize.Width / 2; break; - case XStringAlignment.Near: + case PdfHorizontalAlignment.Near: break; - case XStringAlignment.Far: + case PdfHorizontalAlignment.Far: xOffset -= textSize.Width; break; } - switch (fmt.LineAlignment) + switch (verticalAlignment) { - case XLineAlignment.Center: + case PdfVerticalAlignment.Center: yOffset -= textSize.Height / 2; break; - case XLineAlignment.Near: + case PdfVerticalAlignment.Near: break; - case XLineAlignment.Far: + case PdfVerticalAlignment.Far: yOffset -= textSize.Height; break; } @@ -79,51 +78,53 @@ public static XRect RectFromStringFormat(double x, double y, XSize textSize, XSt return result; } - public static (double, double, double, double) CoordRectToPage(this PdfPage page, double x, double y, double w, double h) + public static (double, double, double, double) CoordRectToPage(double pageWidth, double pageHeight, + double x, double y, double w, double h) { if (x < 0) { - x = page.Width + x; + x = pageWidth + x; } if (y < 0) { - y = page.Height + y; + y = pageHeight + y; } if (w < 0) { - w = page.Width + w - x; + w = pageWidth + w - x; } if (h < 0) { - h = page.Height + h - y; + h = pageHeight + h - y; } return (x, y, w, h); } - public static (double, double, double?, double?) CoordRectToPage(this PdfPage page, double x, double y, double? w, double? h) + public static (double, double, double?, double?) CoordRectToPage(double pageWidth, double pageHeight, + double x, double y, double? w, double? h) { if (x < 0) { - x = page.Width + x; + x = pageWidth + x; } if (y < 0) { - y = page.Height + y; + y = pageHeight + y; } if (w is < 0) { - w = page.Width + w - x; + w = pageWidth + w - x; } if (h is < 0) { - h = page.Height + h - y; + h = pageHeight + h - y; } return (x, y, w, h); diff --git a/PdfSharpDslCore/Drawing/IInstructionBlock.cs b/PdfSharpDslCore/Drawing/IInstructionBlock.cs index d6dcce1..f5115b7 100644 --- a/PdfSharpDslCore/Drawing/IInstructionBlock.cs +++ b/PdfSharpDslCore/Drawing/IInstructionBlock.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; using System.Diagnostics; using Microsoft.Extensions.Logging; -using PdfSharpCore.Drawing; using PdfSharpDslCore.Extensions; namespace PdfSharpDslCore.Drawing { internal interface IInstruction { - XRect Rect { get; } + PdfRect Rect { get; } string Name { get; } /// @@ -51,7 +50,7 @@ internal interface IInstructionBlock : IInstruction /// IInstructionBlock OpenBlock(string name, double offsetY, bool entirePrint, double newPageTopMargin=0); void CloseBlock(); - void UpdateRect(XRect rect); + void UpdateRect(PdfRect rect); void Clear(); diff --git a/PdfSharpDslCore/Drawing/IPdfDocumentDrawer.cs b/PdfSharpDslCore/Drawing/IPdfDocumentDrawer.cs index 060ef2f..1313858 100644 --- a/PdfSharpDslCore/Drawing/IPdfDocumentDrawer.cs +++ b/PdfSharpDslCore/Drawing/IPdfDocumentDrawer.cs @@ -1,8 +1,6 @@  using System; -using PdfSharpCore; -using PdfSharpCore.Drawing; using System.Collections.Generic; namespace PdfSharpDslCore.Drawing @@ -20,7 +18,7 @@ public record TextOrientation public record DrawingResult { - public XRect DrawingRect { get; set; } + public PdfRect DrawingRect { get; set; } public double PageOffsetY { get; set; } } @@ -29,27 +27,27 @@ public interface IPdfDocumentDrawer void DrawRect(double x, double y, double w, double h, bool isFilled); void DrawText(string text, double x, double y, double? w, double? h); void DrawLineText(string text, double x, double y, double? w, double? h, - XStringAlignment hAlign, XLineAlignment vAlign, TextOrientation textOrientation); + PdfHorizontalAlignment hAlign, PdfVerticalAlignment vAlign, TextOrientation textOrientation); void SetViewSize(double w, double h); - XPen CurrentPen { get; set; } - XBrush CurrentBrush { get; set; } - XBrush? HighlightBrush { get; set; } - XFont CurrentFont { get; set; } + PdfPen CurrentPen { get; set; } + PdfBrush CurrentBrush { get; set; } + PdfBrush? HighlightBrush { get; set; } + PdfFont CurrentFont { get; set; } double PageWidth { get; } double PageHeight { get; } DebugOptions DebugOptions { get; set; } - void NewPage(PageSize? pageSize = null, PageOrientation? pageOrientation = null); + void NewPage(PdfPageSize? pageSize = null, PdfPageOrientation? pageOrientation = null); void DrawLine(double x, double y, double x1, double y1); - void DrawTitle(string text, double margin, XStringAlignment hAlign, XLineAlignment vAlign); + void DrawTitle(string text, double margin, PdfHorizontalAlignment hAlign, PdfVerticalAlignment vAlign); void DrawEllipse(double x, double y, double w, double h, bool isFilled); void MoveTo(double x, double y); void LineTo(double x, double y); void DrawTable(double x, double y, TableDefinition tblDef); - void DrawImage(XImage image, double x, double y, double? w, double? h, bool sizeInPixel, bool cropImage); + void DrawImage(PdfImage image, double x, double y, double? w, double? h, bool sizeInPixel, bool cropImage); void DrawPie(double x, double y, double? w, double? h, double startAngle, double sweepAngle, bool isFilled); - void DrawPolygon(IEnumerable points, bool isFilled); + void DrawPolygon(IEnumerable points, bool isFilled); void BeginDrawRowTemplate(string name, int index, double offsetY, double newPageTopMargin); DrawingResult EndDrawRowTemplate(int index); void BeginIterationTemplate(int rowCount); diff --git a/PdfSharpDslCore/Drawing/InstructionBlock.cs b/PdfSharpDslCore/Drawing/InstructionBlock.cs index 61fc640..5221eae 100644 --- a/PdfSharpDslCore/Drawing/InstructionBlock.cs +++ b/PdfSharpDslCore/Drawing/InstructionBlock.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; -using PdfSharpCore.Drawing; using PdfSharpDslCore.Extensions; namespace PdfSharpDslCore.Drawing @@ -12,11 +11,11 @@ namespace PdfSharpDslCore.Drawing [DebuggerDisplay("Rect:{Rect}")] class InstructionAction : IInstruction, IHasName { - public XRect Rect { get; } + public PdfRect Rect { get; } private readonly Action _action; public string Name { get; } - public InstructionAction(Action action, XRect rect, string name) + public InstructionAction(Action action, PdfRect rect, string name) { this.Rect = rect; _action = action; @@ -41,7 +40,7 @@ class InstructionBlock : IInstructionBlock, IHasName public bool ShouldBeEntirePrinted { get; } - public XRect Rect { get; private set; } = XRect.Empty; + public PdfRect Rect { get; private set; } = PdfRect.Empty; public IInstructionBlock? Parent => _parent; public double OffsetY => _offsetY; @@ -85,7 +84,7 @@ public double Draw(IPdfDocumentDrawer drawer, double offsetY, double pageOffsetY { var selfOffsetY = _offsetY; - XRect pageRect = new XRect(0, 0, drawer.PageWidth, drawer.PageHeight); + PdfRect pageRect = new PdfRect(0, 0, drawer.PageWidth, drawer.PageHeight); //2 cas : l'instruction ne rentre pas dans la page actuelle, il faut une nouvelle page // ça ne rentre dans aucune page, il faut "imprimé" par morceau if (ShouldBeEntirePrinted) @@ -292,7 +291,7 @@ public void CloseBlock() // } - public void UpdateRect(XRect rect) + public void UpdateRect(PdfRect rect) { if (rect.IsEmpty) return; rect.Offset(0, _offsetY); diff --git a/PdfSharpDslCore/Drawing/InstructionsRecorder.cs b/PdfSharpDslCore/Drawing/InstructionsRecorder.cs index 148e309..8b65ab9 100644 --- a/PdfSharpDslCore/Drawing/InstructionsRecorder.cs +++ b/PdfSharpDslCore/Drawing/InstructionsRecorder.cs @@ -1,5 +1,4 @@ -using PdfSharpCore.Drawing; -using PdfSharpDslCore.Drawing; +using PdfSharpDslCore.Drawing; using System; using System.Collections.Generic; using System.Linq; diff --git a/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs b/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs index 1bc855c..a8c3527 100644 --- a/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs +++ b/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs @@ -1,940 +1,619 @@ -using PdfSharpCore; -using PdfSharpCore.Drawing; -using PdfSharpCore.Drawing.Layout; -using PdfSharpCore.Drawing.Layout.enums; -using PdfSharpCore.Pdf; -using SixLabors.ImageSharp; +using Microsoft.Extensions.Logging; +using PdfSharpDslCore.Extensions; using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Net; -using Microsoft.Extensions.Logging; -using PdfSharpDslCore.Extensions; -using SixLabors.Fonts; +using TerraPDF.Core; namespace PdfSharpDslCore.Drawing { public sealed class PdfDocumentDrawer : IDisposable, IPdfDocumentDrawer { - private readonly PdfDocument _document; - private readonly ILogger? _logger; - private PdfPage? _currentPage; - private XPen? _currentPen; - private XBrush? _currentBrush; - - private XFont? _currentFont; - private XGraphics? _gfx; - private IXGraphicsRenderer? _gfxRenderer; - private bool _disposedValue; - private XPoint _currentPoint = new XPoint(0, 0); - private PageSize _defaultPageSize = PageSize.A4; - private PageOrientation _defaultPageOrientation = PageOrientation.Portrait; - private readonly List> _onNewPageHooks = new(); + private sealed class RecordedPage + { + public RecordedPage(PdfPageSize size, PdfPageOrientation orientation) + { + (Width, Height) = GetPageDimensions(size, orientation); + } - private readonly DrawingContext _drawingCtx; + public double Width { get; } + public double Height { get; } + public double ScaleX { get; set; } = 1; + public double ScaleY { get; set; } = 1; + public List> Commands { get; } = new(); + } - private readonly XPen _debugPen = new XPen(XColors.Red, 0.5) { DashStyle = XDashStyle.DashDot }; - private readonly Lazy _debugFont = new Lazy(() => new XFont("monospace", 6)); + private readonly ILogger? _logger; + private readonly DrawingContext _drawingCtx; + private readonly List _pages = new(); + private readonly List> _onNewPageHooks = new(); + private readonly Stack _measurementStates = new(); + private PdfPageSize _defaultPageSize = PdfPageSize.A4; + private PdfPageOrientation _defaultPageOrientation = PdfPageOrientation.Portrait; + private PdfPen? _currentPen; + private PdfBrush? _currentBrush; + private PdfFont? _currentFont; + private PdfPoint _currentPoint; + private bool _isMeasuring; - public PdfDocumentDrawer(PdfDocument document, ILogger? logger = null) + public PdfDocumentDrawer(ILogger? logger = null) { - _drawingCtx = new(logger); - _document = document ?? throw new ArgumentNullException(nameof(document)); _logger = logger; + _drawingCtx = new DrawingContext(logger); } - #region properties - public DebugOptions DebugOptions { get => _drawingCtx.DebugOptions; set => _drawingCtx.DebugOptions = value; } - public PdfPage CurrentPage - { - get - { - if (_currentPage is not null) return _currentPage; - _currentPage = _document.AddPage(); - _currentPage.Size = _defaultPageSize; - _currentPage.Orientation = _defaultPageOrientation; - - return _currentPage; - } - set - { - if (_currentPage == value) return; - _currentPage = value; - if (_logger.DebugEnabled()) - { - _logger.WriteDebug(this, $"Dispose GFX:{_gfx?.GetHashCode() ?? 0}"); - } - _gfx?.Dispose(); - _gfx = null; - _gfxRenderer = null; - } - } - - public XPen CurrentPen + public PdfPen CurrentPen { - get => _currentPen ??= XPens.Black; - set => _currentPen = value; + get => _currentPen ??= new PdfPen(PdfColor.Black, 1); + set => _currentPen = value ?? throw new ArgumentNullException(nameof(value)); } - public XBrush CurrentBrush + public PdfBrush CurrentBrush { - get => _currentBrush ??= XBrushes.Black; - set => _currentBrush = value; + get => _currentBrush ??= new PdfBrush(PdfColor.Black); + set => _currentBrush = value ?? throw new ArgumentNullException(nameof(value)); } - public XBrush? HighlightBrush { get; set; } + public PdfBrush? HighlightBrush { get; set; } - public XFont CurrentFont + public PdfFont CurrentFont { - get => _currentFont ??= new XFont("Arial", 10); - set => _currentFont = value; + get => _currentFont ??= new PdfFont("Helvetica", 10); + set => _currentFont = value ?? throw new ArgumentNullException(nameof(value)); } public double PageWidth => CurrentPage.Width; public double PageHeight => CurrentPage.Height; - - private XGraphics Gfx + private RecordedPage CurrentPage { get { - if (_gfx is not null) return _gfx; - if (_logger.DebugEnabled()) + if (_pages.Count == 0) { - _logger.WriteDebug(this, "Create new gfx from page"); + _pages.Add(new RecordedPage(_defaultPageSize, _defaultPageOrientation)); } - _gfx = XGraphics.FromPdfPage(CurrentPage); - // HACK, read from https://github.com/ststeiger/PdfSharpCore/blob/master/docs/MigraDocCore/samples/MixMigraDocCoreAndPDFsharpCore.md - _gfx.MUH = PdfFontEncoding.Unicode; - return _gfx; + return _pages[_pages.Count - 1]; } } - #endregion - - private void Dispose(bool disposing) + public void PublishPdf(Stream stream) { - if (_disposedValue) return; - if (disposing) - { - // TODO: dispose managed state (managed objects) - _gfx?.Dispose(); - } + ArgumentNullException.ThrowIfNull(stream); + CreateDocument().PublishPdf(stream); + } - _gfx = null; - _gfxRenderer = null; - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - _disposedValue = true; + public void PublishPdf(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + CreateDocument().PublishPdf(path); } - public void Dispose() + public byte[] PublishPdf() => CreateDocument().PublishPdf(); + + private DocumentComposer CreateDocument() { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + _ = CurrentPage; + return Document.Create(document => + { + foreach (var recordedPage in _pages) + { + document.Page(page => + { + page.Size(recordedPage.Width, recordedPage.Height); + page.Margin(0); + page.Content().Canvas(recordedPage.Height, canvas => + { + foreach (var command in recordedPage.Commands) + { + command(canvas); + } + }); + }); + } + }); } public void DrawLine(double x, double y, double x1, double y1) { var page = CurrentPage; - if (x < 0) - { - x = page.Width + x; - } - - if (y < 0) - { - y = page.Height + y; - } - - if (x1 < 0) - { - x1 = page.Width + x1; - } - - if (y1 < 0) - { - y1 = page.Height + y1; - } - InternalDrawLine(CurrentPen, x, y, x1, y1); + InternalDrawLine(ScalePen(CurrentPen, page), ScaleX(ResolveX(x, page), page), ScaleY(ResolveY(y, page), page), + ScaleX(ResolveX(x1, page), page), ScaleY(ResolveY(y1, page), page)); } - private void InternalDrawLine(XPen pen, double x, double y, double x1, double y1) + private void InternalDrawLine(PdfPen pen, double x, double y, double x1, double y1) { - Gfx.DrawLine(pen, x, y, x1, y1); - this._drawingCtx.PushInstruction((oy) => InternalDrawLine(pen, x, y+oy, x1, y1+oy), new XRect(new XPoint(x, y), new XPoint(x1, y1))); + AddCommand(canvas => DrawStyledLine(canvas, pen, x, y, x1, y1)); + _drawingCtx.PushInstruction(offset => InternalDrawLine(pen, x, y + offset, x1, y1 + offset), + new PdfRect(new PdfPoint(x, y), new PdfPoint(x1, y1))); } - public void DrawRect(double x, double y, double w, double h, bool isFilled) + private static void DrawStyledLine(VectorCanvas canvas, PdfPen pen, double x, double y, double x1, double y1) { - (x, y, w, h) = CurrentPage.CoordRectToPage(x, y, w, h); - InternalDrawRect(CurrentPen, CurrentBrush, x, y, w, h, isFilled); + canvas.Line(x, y, x1, y1, pen.Color.Hex, pen.Width, pen.Color.Opacity, GetDashPattern(pen)); } - private void InternalDrawRect(XPen pen, XBrush brush, double x, double y, double w, double h, bool isFilled) + private static double[]? GetDashPattern(PdfPen pen) { - if (isFilled) - { - Gfx.DrawRectangle(pen, brush, x, y, w, h); - } - else + var width = Math.Max(pen.Width, 0.1); + return pen.DashStyle switch { - Gfx.DrawRectangle(pen, x, y, w, h); - } - this._drawingCtx.PushInstruction((oy) => InternalDrawRect(pen, brush, x, y+oy, w, h, isFilled), new XRect(x, y, w, h)); + PdfDashStyle.Dash => new[] { 4 * width, 3 * width }, + PdfDashStyle.Dot => new[] { width, 2 * width }, + PdfDashStyle.DashDot => new[] { 4 * width, 3 * width, width, 3 * width }, + PdfDashStyle.DashDotDot => new[] { 4 * width, 3 * width, width, 2 * width, width, 2 * width }, + _ => null, + }; } - public void DrawEllipse(double x, double y, double w, double h, bool isFilled) + public void DrawRect(double x, double y, double w, double h, bool isFilled) { - (x, y, w, h) = CurrentPage.CoordRectToPage(x, y, w, h); - var r = new XRect(x, y, w, h); - InternalDrawEllipse(isFilled, r, CurrentPen, CurrentBrush); + var page = CurrentPage; + (x, y, w, h) = DrawingHelper.CoordRectToPage(page.Width, page.Height, x, y, w, h); + InternalDrawRect(ScalePen(CurrentPen, page), CurrentBrush, ScaleX(x, page), ScaleY(y, page), ScaleX(w, page), ScaleY(h, page), isFilled); } - private void InternalDrawEllipse(bool isFilled, XRect r, XPen pen, XBrush brush) + private void InternalDrawRect(PdfPen pen, PdfBrush brush, double x, double y, double w, double h, bool isFilled) { - if (isFilled) + var dashPattern = GetDashPattern(pen); + AddCommand(canvas => { - Gfx.DrawEllipse(pen, brush, r); - } - else - { - Gfx.DrawEllipse(pen, r); - } - this._drawingCtx.PushInstruction((oy) => InternalDrawEllipse(isFilled, r.OffsetY(oy), pen, brush), r); + if (isFilled) canvas.FillRect(x, y, w, h, brush.Color.Hex, brush.Color.Opacity); + canvas.StrokeRect(x, y, w, h, pen.Color.Hex, pen.Width, pen.Color.Opacity, dashPattern); + }); + _drawingCtx.PushInstruction(offset => InternalDrawRect(pen, brush, x, y + offset, w, h, isFilled), + new PdfRect(x, y, w, h)); } - public void DrawText(string text, double x, double y, double? w, - double? h) + public void DrawEllipse(double x, double y, double w, double h, bool isFilled) { var page = CurrentPage; - (x, y, w, h) = page.CoordRectToPage(x, y, w, h); - //20221009 : only top left is supported - if (w == null || h == null) - { - //because of missing w/h alignment is Left - var sizeFormatter = new XTextSegmentFormatter(Gfx) - { - Alignment = XParagraphAlignment.Left - }; - w = w ?? page.Width - x; - var size = sizeFormatter.CalculateTextSize(text, CurrentFont, CurrentBrush, w.Value); - var r = new XRect(x, y, size.Width, size.Height); - - sizeFormatter.DrawString(text, CurrentFont, CurrentBrush, r); - if (_drawingCtx.DebugText) - { - DebugRect(r); - } - } - else - { - //fmt is not used, because DrawString support only TopLeft - var r = new XRect(x, y, w.Value, h.Value); - var formatter = new XTextFormatter(Gfx); - formatter.DrawString(text, CurrentFont, CurrentBrush, r); - if (_drawingCtx.DebugText) - { - DebugRect(r); - } - } + (x, y, w, h) = DrawingHelper.CoordRectToPage(page.Width, page.Height, x, y, w, h); + InternalDrawEllipse(ScalePen(CurrentPen, page), CurrentBrush, ScaleX(x, page), ScaleY(y, page), ScaleX(w, page), ScaleY(h, page), isFilled); } - public void DrawLineText(string text, double x, double y, double? w, double? h, XStringAlignment hAlign, - XLineAlignment vAlign, TextOrientation textOrientation) + private void InternalDrawEllipse(PdfPen pen, PdfBrush brush, double x, double y, double w, double h, bool isFilled) { - (x, y, w, h) = CurrentPage.CoordRectToPage(x, y, w, h); - var fmt = new XStringFormat + AddCommand(canvas => { - Alignment = hAlign, - LineAlignment = vAlign, - }; - InternalDrawLineText(text, x, y, w, h, textOrientation, fmt, CurrentFont, CurrentBrush, HighlightBrush); + var cx = x + w / 2; + var cy = y + h / 2; + if (isFilled) canvas.FillEllipse(cx, cy, w / 2, h / 2, brush.Color.Hex, brush.Color.Opacity); + canvas.StrokeEllipse(cx, cy, w / 2, h / 2, pen.Color.Hex, pen.Width, pen.Color.Opacity); + }); + _drawingCtx.PushInstruction(offset => InternalDrawEllipse(pen, brush, x, y + offset, w, h, isFilled), new PdfRect(x, y, w, h)); } - private void InternalDrawLineText(string text, double x, double y, double? w, double? h, - TextOrientation textOrientation, XStringFormat fmt, XFont font, XBrush brush, XBrush? hb) + public void DrawText(string text, double x, double y, double? w, double? h) { - XRect r; - //TODO : optimize to do this only on vertical text - var cnt = Gfx.BeginContainer(); - try - { - r = InternalDrawString(text, x, y, w, h, fmt, textOrientation, font, brush, hb); - } - finally - { - Gfx.EndContainer(cnt); - } - - this._drawingCtx.PushInstruction( - (oy) => InternalDrawLineText(text, x, y+oy, w, h, textOrientation, fmt, font, brush, hb), r, instrName:$"DrawLineText({text})"); + var page = CurrentPage; + (x, y, w, h) = DrawingHelper.CoordRectToPage(page.Width, page.Height, x, y, w, h); + InternalDrawText(text, ScaleX(x, page), ScaleY(y, page), w.HasValue ? ScaleX(w.Value, page) : page.Width - x, + h.HasValue ? ScaleY(h.Value, page) : null, PdfHorizontalAlignment.Near, PdfVerticalAlignment.Near, + ScaleFont(CurrentFont, page), CurrentBrush, null); } - private XRect InternalDrawString(string text, double x, double y, double? w, double? h, - XStringFormat fmt, TextOrientation textOrientation, XFont font, XBrush brush, XBrush? hb) + public void DrawLineText(string text, double x, double y, double? w, double? h, PdfHorizontalAlignment hAlign, + PdfVerticalAlignment vAlign, TextOrientation textOrientation) { - XRect result; - double angle = 0; - - if (textOrientation.Angle is not null) - { - angle = textOrientation.Angle.Value; - } - else if (textOrientation.Orientation == TextOrientationEnum.Vertical) - { - angle = 90; - } - - if (angle != 0) - { - Gfx.RotateAtTransform(angle, new XPoint(x, y)); - } - - var textSize = Gfx.MeasureString(text, font); - if (w == null || h == null) - { - Gfx.DrawString(text, font, brush, x, y, fmt); - var rText = new XRect(x, y, textSize.Width, textSize.Height); - var r = DrawingHelper.RectFromStringFormat(x, y, textSize, fmt); - result = r; - if (_drawingCtx.DebugText) - { - DebugRect(r); - } - - if (hb == null) return result; - - - // - //var highlightColor = XColor.FromArgb(50, 255, 233, 178); - //var b = new XSolidBrush(highlightColor); - Gfx.DrawRectangle(hb, r); - } - else - { - //fmt is not used, because DrawString support only TopLeft - var r = new XRect(x, y, w.Value, h.Value); - Gfx.DrawString(text, font, brush, r, fmt); - var hr = DrawingHelper.RectFromStringFormat(x, y, textSize, fmt); - result = hr; - if (_drawingCtx.DebugText) + var page = CurrentPage; + (x, y, w, h) = DrawingHelper.CoordRectToPage(page.Width, page.Height, x, y, w, h); + InternalDrawText(text, ScaleX(x, page), ScaleY(y, page), w.HasValue ? ScaleX(w.Value, page) : null, + h.HasValue ? ScaleY(h.Value, page) : null, hAlign, vAlign, ScaleFont(CurrentFont, page), CurrentBrush, HighlightBrush, + textOrientation?.Angle ?? textOrientation?.Orientation switch { - DebugRect(hr); - } - - if (hb == null) return result; - hr.Intersect(r); - //var highlightColor = XColor.FromArgb(50, 255, 233, 178); - //var b = new XSolidBrush(highlightColor); - Gfx.DrawRectangle(hb, hr); - } - - return result; + TextOrientationEnum.Vertical => 90, + TextOrientationEnum.HorizontalInvert => 180, + TextOrientationEnum.VerticalInvert => 270, + _ => 0, + }); } - public void DrawTitle(string text, double margin, XStringAlignment hAlign, XLineAlignment vAlign) + private void InternalDrawText(string text, double x, double y, double? w, double? h, PdfHorizontalAlignment hAlign, + PdfVerticalAlignment vAlign, PdfFont font, PdfBrush brush, PdfBrush? highlight, double angle = 0) { - var page = CurrentPage; + var lines = WrapText(text, w, font); + var lineHeight = font.Size * 1.2; + var measuredWidth = lines.Count == 0 ? 0 : lines.Max(line => MeasureText(line, font)); + var measuredHeight = lines.Count * lineHeight; + var rect = new PdfRect(x, y, w ?? measuredWidth, h ?? measuredHeight); + var textSize = new PdfSize(Math.Min(measuredWidth, rect.Width), Math.Min(measuredHeight, rect.Height)); + var textRect = w.HasValue + ? DrawingHelper.RectFromStringFormat(rect, textSize, hAlign, vAlign) + : DrawingHelper.RectFromStringFormat(x, y, textSize, hAlign, vAlign); - var fmt = new XStringFormat + AddCommand(canvas => { - Alignment = hAlign, - LineAlignment = vAlign, - }; + if (highlight is not null) + canvas.FillRect(textRect.X, textRect.Y, textRect.Width, textRect.Height, highlight.Color.Hex, highlight.Color.Opacity); - var textSize = Gfx.MeasureString(text, CurrentFont, XStringFormats.TopLeft); - if (margin < 0) - { - margin = page.Height - textSize.Height + margin; - } - - var r = new XRect(0, margin, page.Width, textSize.Height); - InternalDrawText(text, r, fmt, textSize, CurrentFont, CurrentBrush, HighlightBrush); - } - - private void InternalDrawText(string text, XRect r, XStringFormat fmt, XSize textSize, - XFont font, XBrush brush, XBrush? hb) - { - Gfx.DrawString(text, font, brush, r, fmt); - this._drawingCtx.PushInstruction((oy) => InternalDrawText(text, r.OffsetY(oy), fmt, textSize, font, brush, hb), r); - if (_drawingCtx.DebugText) - { - var debugRect = DrawingHelper.RectFromStringFormat(r, textSize, fmt); - DebugRect(debugRect); - } - - if (hb == null) return; - - var hr = DrawingHelper.RectFromStringFormat(r, textSize, fmt); - Gfx.DrawRectangle(hb, hr); - } - - public void DrawTable(double x, double y, TableDefinition tblDef) - { - var availableWidth = PageWidth - x; - Gfx.Save(); - try - { - //todo check if the current page can receive - //calculate table dimension - var defaultFont = CurrentFont; - var defaultBrush = CurrentBrush; - - XFont[] xFonts = new XFont[tblDef.Columns.Count]; - bool[] colMeasure = new bool[tblDef.Columns.Count]; - bool calcHeaderHeight = tblDef.HeaderHeight is null; - var margins = tblDef.CellMargin; - int i = 0; - foreach (var column in tblDef.Columns) + for (var index = 0; index < lines.Count; index++) { - xFonts[i] = column.Font ?? defaultFont; - colMeasure[i] = column.DesiredWidth is null; - if (colMeasure[i] || calcHeaderHeight) - { - var measure = Gfx.MeasureString(column.ColumnHeaderName, xFonts[i]); - if (colMeasure[i]) + var line = lines[index]; + if (string.IsNullOrWhiteSpace(line)) continue; + var lineWidth = MeasureText(line, font); + var lineX = w.HasValue + ? hAlign switch { - column.DesiredWidth = measure.Width + margins.Left + margins.Right; + PdfHorizontalAlignment.Center => rect.X + (rect.Width - lineWidth) / 2, + PdfHorizontalAlignment.Far => rect.Right - lineWidth, + _ => rect.X, } - - if (calcHeaderHeight) + : hAlign switch { - tblDef.HeaderHeight = Math.Max(tblDef.HeaderHeight ?? 0, - measure.Height + margins.Top + margins.Bottom); - } - } - - i++; + PdfHorizontalAlignment.Center => x - lineWidth / 2, + PdfHorizontalAlignment.Far => x - lineWidth, + _ => x, + }; + canvas.Text(line, lineX, textRect.Y + font.Size + index * lineHeight, brush.Color.Hex, font.Size, + font.FamilyName, font.Style.HasFlag(PdfFontStyle.Bold), font.Style.HasFlag(PdfFontStyle.Italic), brush.Color.Opacity, + angle); } + }); - var placements = LayoutTableCells(tblDef); - - //measure all rows - var sizeFormatter = new XTextSegmentFormatter(Gfx) - { - Alignment = XParagraphAlignment.Left - }; - for (var rowIndex = 0; rowIndex < tblDef.Rows.Count; rowIndex++) - { - var row = tblDef.Rows[rowIndex]; - var rowMeasure = row.DesiredHeight is null; - foreach (var placement in placements) - { - //a spanning cell does not inflate a single column; measured separately below - if (placement.Row != rowIndex || placement.ColumnSpan != 1) continue; - var colIndex = placement.Column; - var text = placement.Cell.Text; - var testSize = !colMeasure[colIndex]; - var pageSpaceLeft = tblDef.ColMaxWidth(colIndex, availableWidth); - var column = tblDef.Columns[colIndex]; - if (colMeasure[colIndex]) - { - var cSize = Gfx.MeasureString(text, xFonts[colIndex]); - cSize.Width += (margins.Left + margins.Right); - if (cSize.Width > pageSpaceLeft) - { - //measure height with fixed width - testSize = true; - column.DesiredWidth = Math.Max(column.DesiredWidth ?? 0, pageSpaceLeft); - } - else - { - column.DesiredWidth = Math.Max(column.DesiredWidth ?? 0, cSize.Width); - } - - if (rowMeasure && placement.RowSpan == 1) - { - row.DesiredHeight = Math.Max(row.DesiredHeight ?? 0, - cSize.Height + margins.Top + margins.Bottom); - } - } - - if (!testSize) continue; - var w = Math.Max(0, Math.Min(column.DesiredWidth ?? 0, pageSpaceLeft)); - var measure = SafeCalculateTextSize(sizeFormatter, text, xFonts[colIndex], defaultBrush, w); - if (rowMeasure && placement.RowSpan == 1) - { - row.DesiredHeight = Math.Max(row.DesiredHeight ?? 0, - measure.Height + margins.Top + margins.Bottom); - } - } - } - - //column widths are final: build x-offsets, then grow rows for cells spanning columns and/or rows - var colX = new double[tblDef.Columns.Count + 1]; - for (i = 0; i < tblDef.Columns.Count; i++) - { - colX[i + 1] = colX[i] + tblDef.Columns[i].DrawWidth; - } - - foreach (var placement in placements) - { - if (placement.ColumnSpan == 1 && placement.RowSpan == 1) continue; - var spanWidth = colX[placement.Column + placement.ColumnSpan] - colX[placement.Column]; - var w = Math.Max(0, spanWidth - margins.Left - margins.Right); - var measure = SafeCalculateTextSize(sizeFormatter, placement.Cell.Text, xFonts[placement.Column], - defaultBrush, w); - var requiredHeight = measure.Height + margins.Top + margins.Bottom; - var spannedRows = tblDef.Rows.Skip(placement.Row).Take(placement.RowSpan).ToArray(); - var missingHeight = requiredHeight - spannedRows.Sum(r => r.DesiredHeight ?? 0); - if (missingHeight > 0) - { - var lastSpannedRow = spannedRows[spannedRows.Length - 1]; - lastSpannedRow.DesiredHeight = (lastSpannedRow.DesiredHeight ?? 0) + missingHeight; - } - } + _drawingCtx.PushInstruction(offset => InternalDrawText(text, x, y + offset, w, h, hAlign, vAlign, font, brush, highlight, angle), + textRect, instrName: $"DrawText({text})"); + } - //draw header - double offsetX = 0; - double offsetY = 0; - i = 0; - if (y + tblDef.HeaderHeight > CurrentPage.Height) - { - Gfx.Restore(); - NewPage(); - Gfx.Save(); - //TODO: set top margin - y = 1; - } + public void DrawTitle(string text, double margin, PdfHorizontalAlignment hAlign, PdfVerticalAlignment vAlign) + { + var height = CurrentFont.Size * 1.2; + if (margin < 0) margin = PageHeight - height + margin; + InternalDrawText(text, 0, margin, PageWidth, height, hAlign, vAlign, CurrentFont, CurrentBrush, HighlightBrush); + } - foreach (var column in tblDef.Columns) - { - var w = column.DrawWidth; - var h = tblDef.HeaderHeight ?? 0; - - var r = new XRect(offsetX + x, y, w, h); - var hMargin = margins.Left + margins.Right; - var vMargin = margins.Top + margins.Bottom; - var rText = new XRect(offsetX + x + margins.Left, offsetY + y + margins.Top, w - hMargin, - h - vMargin); - - Gfx.DrawRectangle(CurrentPen, tblDef.HeaderBackColor, r); - offsetX += column.DrawWidth; - //todo: alignment - var fmt = new XStringFormat - { Alignment = XStringAlignment.Center, LineAlignment = XLineAlignment.Center }; - DrawStringMultiline(column.ColumnHeaderName, xFonts[i], column.Brush ?? defaultBrush, rText, fmt); - i++; - } + public void DrawTable(double x, double y, TableDefinition table) + { + ArgumentNullException.ThrowIfNull(table); + var availableWidth = PageWidth - x; + var fonts = table.Columns.Select(column => column.Font ?? CurrentFont).ToArray(); + var margins = table.CellMargin; + var cells = LayoutTableCells(table); - offsetY = tblDef.HeaderHeight ?? 0; - //draw body; a rowspan/colspan cell is drawn once, at the row/column where it starts - for (var rowIndex = 0; rowIndex < tblDef.Rows.Count; rowIndex++) - { - var row = tblDef.Rows[rowIndex]; - var rowPlacements = placements.Where(p => p.Row == rowIndex).ToArray(); - //a row fully covered by a rowspan started above has no placements of its own: never split a rowspan block - if (rowPlacements.Length > 0) - { - var blockHeight = rowPlacements.Max(p => - tblDef.Rows.Skip(p.Row).Take(p.RowSpan).Sum(sr => sr.DesiredHeight ?? 0)); - if (y + offsetY + blockHeight > CurrentPage.Height) - { - Gfx.Restore(); - NewPage(); - Gfx.Save(); - //TODO: set top margin - y = 1; - offsetY = 0; - } - } + for (var index = 0; index < table.Columns.Count; index++) + { + var column = table.Columns[index]; + var contentWidth = cells + .Where(cell => cell.Column == index && cell.Cell.ColumnSpan == 1) + .Select(cell => MeasureCellWidth(cell.Cell.Text, fonts[index])) + .DefaultIfEmpty(0) + .Max(); + var desiredWidth = Math.Max(MeasureCellWidth(column.ColumnHeaderName, fonts[index]), contentWidth) + + margins.Left + margins.Right; + column.DesiredWidth ??= Math.Min(desiredWidth, + table.ColMaxWidth(index, availableWidth)); + table.HeaderHeight ??= fonts[index].Size * 1.2 + margins.Top + margins.Bottom; + } - foreach (var placement in rowPlacements) + for (var rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++) + { + var row = table.Rows[rowIndex]; + var contentHeight = cells.Where(cell => cell.Row == rowIndex && cell.Cell.RowSpan == 1) + .Select(cell => { - DrawTableCell(x, y + offsetY, placement, tblDef, xFonts, defaultBrush, colX); - } - - offsetY += row.DesiredHeight ?? 0; - } + var width = table.Columns.Skip(cell.Column).Take(cell.Cell.ColumnSpan).Sum(column => column.DrawWidth); + var font = fonts[cell.Column]; + return MeasureCellLineCount(cell.Cell.Text, width - margins.Left - margins.Right, font) + * font.Size * 1.2 + margins.Top + margins.Bottom; + }).DefaultIfEmpty(fonts[0].Size * 1.2 + margins.Top + margins.Bottom).Max(); + row.DesiredHeight ??= contentHeight; } - finally + + foreach (var cell in cells.Where(cell => cell.Cell.RowSpan > 1)) { - Gfx.Restore(); + var width = table.Columns.Skip(cell.Column).Take(cell.Cell.ColumnSpan).Sum(column => column.DrawWidth); + var font = fonts[cell.Column]; + var requiredHeight = MeasureCellLineCount(cell.Cell.Text, width - margins.Left - margins.Right, font) + * font.Size * 1.2 + margins.Top + margins.Bottom; + var rows = table.Rows.Skip(cell.Row).Take(cell.Cell.RowSpan).ToArray(); + var missingHeight = requiredHeight - rows.Sum(row => row.DesiredHeight ?? 0); + if (missingHeight > 0) + rows[^1].DesiredHeight = (rows[^1].DesiredHeight ?? 0) + missingHeight; } - } - private sealed class TableCellPlacement - { - public TableCellPlacement(int row, int column, int columnSpan, int rowSpan, CellDefinition cell) + if (y + (table.HeaderHeight ?? 0) > PageHeight) { NewPage(); y = 1; } + var offsetY = 0d; + if (table.ShowHeader) { - Row = row; - Column = column; - ColumnSpan = columnSpan; - RowSpan = rowSpan; - Cell = cell; + DrawTableRow(x, y, table.Columns.Select(column => column.ColumnHeaderName).ToArray(), table.HeaderHeight ?? 0, table, fonts, table.HeaderBackColor); + offsetY = table.HeaderHeight ?? 0; } - public int Row { get; } - public int Column { get; } - public int ColumnSpan { get; } - public int RowSpan { get; } - public CellDefinition Cell { get; } + for (var rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++) + { + var row = table.Rows[rowIndex]; + var height = row.DesiredHeight ?? 0; + var rowCells = cells.Where(cell => cell.Row == rowIndex).ToArray(); + var requiredHeight = rowCells.Select(cell => table.Rows.Skip(rowIndex).Take(cell.Cell.RowSpan) + .Sum(spannedRow => spannedRow.DesiredHeight ?? 0)).DefaultIfEmpty(height).Max(); + if (y + offsetY + requiredHeight > PageHeight) { NewPage(); y = table.TopMarginOnPageBreak; offsetY = 0; } + foreach (var cell in rowCells) + DrawTableCell(x, y + offsetY, cell, table, fonts); + offsetY += height; + } } private static List LayoutTableCells(TableDefinition table) { var result = new List(); - var columnCount = table.Columns.Count; - var occupiedUntilRow = new int[columnCount]; + var occupiedUntilRow = new int[table.Columns.Count]; for (var rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++) { var row = table.Rows[rowIndex]; var cells = row.Cells.Count > 0 ? row.Cells : row.Data.Select(text => new CellDefinition { Text = text }).ToList(); - var columnIndex = 0; foreach (var cell in cells) { - while (columnIndex < columnCount && occupiedUntilRow[columnIndex] > rowIndex) + while (columnIndex < table.Columns.Count && occupiedUntilRow[columnIndex] > rowIndex) columnIndex++; - if (columnIndex >= columnCount) break; - - var columnSpan = Math.Min(Math.Max(1, cell.ColumnSpan), columnCount - columnIndex); - var rowSpan = Math.Min(Math.Max(1, cell.RowSpan), table.Rows.Count - rowIndex); - result.Add(new TableCellPlacement(rowIndex, columnIndex, columnSpan, rowSpan, cell)); - for (var c = columnIndex; c < columnIndex + columnSpan; c++) - occupiedUntilRow[c] = rowIndex + rowSpan; - columnIndex += columnSpan; - } - - //keep drawing borders for any column this (short) row never reached - for (var c = 0; c < columnCount; c++) - { - if (occupiedUntilRow[c] > rowIndex) continue; - result.Add(new TableCellPlacement(rowIndex, c, 1, 1, new CellDefinition())); - occupiedUntilRow[c] = rowIndex + 1; + if (columnIndex >= table.Columns.Count) break; + + cell.ColumnSpan = Math.Min(Math.Max(1, cell.ColumnSpan), table.Columns.Count - columnIndex); + cell.RowSpan = Math.Min(Math.Max(1, cell.RowSpan), table.Rows.Count - rowIndex); + result.Add(new TableCellPlacement(rowIndex, columnIndex, cell)); + for (var index = columnIndex; index < columnIndex + cell.ColumnSpan; index++) + occupiedUntilRow[index] = rowIndex + cell.RowSpan; + columnIndex += cell.ColumnSpan; } } - return result; } - private void DrawTableCell(double x, double y, TableCellPlacement placement, TableDefinition table, - XFont[] fonts, XBrush defaultBrush, double[] colX) + private void DrawTableCell(double x, double y, TableCellPlacement placement, TableDefinition table, PdfFont[] fonts) { var column = table.Columns[placement.Column]; - var margins = table.CellMargin; - var w = colX[placement.Column + placement.ColumnSpan] - colX[placement.Column]; - var h = table.Rows.Skip(placement.Row).Take(placement.RowSpan).Sum(r => r.DesiredHeight ?? 0); - var r = new XRect(x + colX[placement.Column], y, w, h); - ResetClip(); - Gfx.DrawRectangle(CurrentPen, column.BackColor, r); - var hMargin = margins.Left + margins.Right; - var vMargin = margins.Top + margins.Bottom; - var rText = new XRect(r.X + margins.Left, r.Y + margins.Top, w - hMargin, h - vMargin); - Gfx.IntersectClip(rText); - var fmt = new XStringFormat + var offsetX = table.Columns.Take(placement.Column).Sum(item => item.DrawWidth); + var width = table.Columns.Skip(placement.Column).Take(placement.Cell.ColumnSpan).Sum(item => item.DrawWidth); + var height = table.Rows.Skip(placement.Row).Take(placement.Cell.RowSpan).Sum(row => row.DesiredHeight ?? 0); + if (column.BackColor is not null) { - Alignment = placement.Cell.HorizontalAlignment ?? column.Alignment, - LineAlignment = placement.Cell.VerticalAlignment ?? XLineAlignment.Near - }; - DrawStringMultiline(placement.Cell.Text, fonts[placement.Column], column.Brush ?? defaultBrush, rText, - fmt); - } - - /// - /// Some fallback fonts (e.g. a missing "Arial" on Linux CI resolving to a font with degenerate metrics) - /// make PdfSharpCore's own wrap-height computation go negative and throw. Fall back to an unwrapped - /// measurement, which does not exercise that code path, rather than crash the whole table. - /// - private XSize SafeCalculateTextSize(XTextSegmentFormatter formatter, string text, XFont font, XBrush brush, - double width) - { - try - { - return formatter.CalculateTextSize(text, font, brush, width); - } - catch (ArgumentException) - { - return Gfx.MeasureString(text, font); + var previousBrush = CurrentBrush; + CurrentBrush = column.BackColor; + DrawRect(x + offsetX, y, width, height, true); + CurrentBrush = previousBrush; } - } + else DrawRect(x + offsetX, y, width, height, false); - private void ResetClip() - { - if (_gfx is null) return; - _gfxRenderer ??= (IXGraphicsRenderer)_gfx.GetType().GetField("_renderer", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.GetField - | System.Reflection.BindingFlags.Instance) - .GetValue(_gfx); - _gfxRenderer?.ResetClip(); + var margins = table.CellMargin; + InternalDrawText(placement.Cell.Text, x + offsetX + margins.Left, y + margins.Top, + Math.Max(0, width - margins.Left - margins.Right), Math.Max(0, height - margins.Top - margins.Bottom), + placement.Cell.HorizontalAlignment ?? column.Alignment, + placement.Cell.VerticalAlignment ?? PdfVerticalAlignment.Center, + fonts[placement.Column], column.Brush ?? CurrentBrush, null); } - private void DrawStringMultiline(string text, XFont xFont, XBrush xBrush, XRect r, XStringFormat fmt) + private sealed record TableCellPlacement(int Row, int Column, CellDefinition Cell); + + private void DrawTableRow(double x, double y, string[] values, double height, TableDefinition table, PdfFont[] fonts, PdfBrush? rowBackground) { - var formatter = new XTextFormatter(Gfx) + var offsetX = 0d; + for (var index = 0; index < table.Columns.Count; index++) { - Alignment = fmt.Alignment switch - { - XStringAlignment.Center => XParagraphAlignment.Center, - XStringAlignment.Far => XParagraphAlignment.Right, - _ => XParagraphAlignment.Left - }, - VerticalAlignment = fmt.LineAlignment switch + var column = table.Columns[index]; + var width = column.DrawWidth; + var background = rowBackground ?? column.BackColor; + if (background is not null) { - XLineAlignment.Center => XVerticalAlignment.Middle, - XLineAlignment.Far => XVerticalAlignment.Bottom, - _ => XVerticalAlignment.Top + var previousBrush = CurrentBrush; + CurrentBrush = background; + DrawRect(x + offsetX, y, width, height, true); + CurrentBrush = previousBrush; } - }; - formatter.DrawString(text, xFont, xBrush, r); + else DrawRect(x + offsetX, y, width, height, false); + + var margins = table.CellMargin; + InternalDrawText(index < values.Length ? values[index] : string.Empty, x + offsetX + margins.Left, y + margins.Top, + Math.Max(0, width - margins.Left - margins.Right), Math.Max(0, height - margins.Top - margins.Bottom), + column.Alignment, PdfVerticalAlignment.Center, fonts[index], column.Brush ?? CurrentBrush, null); + offsetX += width; + } } public void SetViewSize(double w, double h) { - var scaleX = CurrentPage.Width / w; - var scaleY = CurrentPage.Height / h; - - Gfx.ScaleTransform(scaleX, scaleY); + if (w <= 0 || h <= 0) throw new ArgumentOutOfRangeException(nameof(w), "View dimensions must be positive."); + CurrentPage.ScaleX = CurrentPage.Width / w; + CurrentPage.ScaleY = CurrentPage.Height / h; } - public void NewPage(PageSize? pageSize = null, PageOrientation? pageOrientation = null) + public void NewPage(PdfPageSize? pageSize = null, PdfPageOrientation? pageOrientation = null) { _defaultPageSize = pageSize ?? _defaultPageSize; _defaultPageOrientation = pageOrientation ?? _defaultPageOrientation; - - CurrentPage = AddPage(); - _onNewPageHooks.ForEach( x=> x(_document.PageCount)); + _pages.Add(new RecordedPage(_defaultPageSize, _defaultPageOrientation)); + _logger?.WriteDebug(this, "AddPage"); + _onNewPageHooks.ForEach(callback => callback(_pages.Count)); if ((DebugOptions & DebugOptions.DebugRule) == DebugOptions.DebugRule) - { - var mm5 = 25; - var start = mm5; - while (start < PageHeight) - { - var ten = (start % (mm5+mm5)) == 0; - this.Gfx.DrawLine(XPens.Red, 0, start, ten ? 50 : 25, start); - if (ten) - { - DebugText($"{start}", 50, start); - } - start += mm5; - } - } + for (var position = 25d; position < PageHeight; position += 25) + DrawLine(0, position, position % 50 == 0 ? 50 : 25, position); } - private PdfPage AddPage() - { - if (_logger.DebugEnabled()) - { - _logger.WriteDebug(this, $"AddPage"); - } - var page = _document.AddPage(); - page.Size = _defaultPageSize; - page.Orientation = _defaultPageOrientation; - return page; - } - - public void MoveTo(double x, double y) - { - _currentPoint = new XPoint(x, y); - } + public void MoveTo(double x, double y) => _currentPoint = new PdfPoint(x, y); public void LineTo(double x, double y) { - var endPoint = new XPoint(x, y); - InternalLineTo(_currentPoint, endPoint, CurrentPen); + var page = CurrentPage; + var endPoint = new PdfPoint(x, y); + InternalDrawLine(ScalePen(CurrentPen, page), ScaleX(_currentPoint.X, page), ScaleY(_currentPoint.Y, page), + ScaleX(endPoint.X, page), ScaleY(endPoint.Y, page)); _currentPoint = endPoint; } - private void InternalLineTo(XPoint p1, XPoint p2, XPen pen) + public void DrawImage(PdfImage image, double x, double y, double? w, double? h, bool sizeInPixel, bool cropImage) { - Gfx.DrawLine(pen, p1, p2); - this._drawingCtx.PushInstruction((oy) => InternalLineTo(p1.OffsetY(oy), p2.OffsetY(oy), pen), new XRect(p1, p2)); - //this._drawingCtx.UpdateDrawingRect(new XRect(p1, p2)); + ArgumentNullException.ThrowIfNull(image); + var page = CurrentPage; + (x, y, w, h) = DrawingHelper.CoordRectToPage(page.Width, page.Height, x, y, w, h); + var data = image.Data.ToArray(); + var naturalSize = VectorCanvas.GetImageSizeInPoints(data); + var width = w.HasValue ? (sizeInPixel ? w.Value * 72d / 96d : w.Value) : naturalSize.Width; + var height = h.HasValue ? (sizeInPixel ? h.Value * 72d / 96d : h.Value) : naturalSize.Height; + if (width <= 0 || height <= 0) + throw new ArgumentOutOfRangeException(nameof(w), "Image dimensions must be positive."); + + var fit = cropImage ? ImageFit.CropTopLeft : ImageFit.Stretch; + AddCommand(canvas => canvas.Image(data, ScaleX(x, page), ScaleY(y, page), + ScaleX(width, page), ScaleY(height, page), fit)); + _drawingCtx.PushInstruction(offset => DrawImage(image, x, y + offset, w, h, sizeInPixel, cropImage), + new PdfRect(x, y, width, height), instrName: "DrawImage"); } - public void DrawImage(XImage image, double x, double y, double? w, double? h, bool sizeInPixel, bool cropImage) + public void DrawPie(double x, double y, double? w, double? h, double startAngle, double sweepAngle, bool isFilled) { - if (sizeInPixel) - { - //convert Pixel to Point - w = w * 72 / 96.0; - h = h * 72 / 96.0; - /* - if (h is not null) - { - h = (double)(h * 72) / 96.0; - } - */ - } - - //fix coord if < 0 - (x, y, w, h) = CurrentPage.CoordRectToPage(x, y, w, h); - InternalDrawImage(image, x, y, w, h, cropImage); + var page = CurrentPage; + var width = w ?? 0; + var height = h ?? 0; + if (width <= 0 || height <= 0) + throw new ArgumentOutOfRangeException(nameof(w), "Pie dimensions must be positive."); + InternalDrawPie(ScalePen(CurrentPen, page), CurrentBrush, ScaleX(x, page), ScaleY(y, page), + ScaleX(width, page), ScaleY(height, page), startAngle, sweepAngle, isFilled); } - private void InternalDrawImage(XImage image, double x, double y, double? w, double? h, bool cropImage) + private void InternalDrawPie(PdfPen pen, PdfBrush brush, double x, double y, double width, double height, + double startAngle, double sweepAngle, bool isFilled) { - var ow = w; - var oh = h; - if (w is null && h is null) + AddCommand(canvas => { - Gfx.DrawImage(image, x, y); - w = image.PointWidth; - h = image.PointHeight; - } - else - { - w ??= image.PointWidth; - h ??= image.PointHeight; - if (cropImage) - { - //draw in form, then draw form in page - using XForm form = new XForm(this._document, XUnit.FromPoint(w.Value), XUnit.FromPoint(h.Value)); - using var gr = XGraphics.FromForm(form); - gr.DrawImage(image, 0, 0); - Gfx.DrawImage(form, x, y); - } + if (isFilled) + canvas.DrawPie(x, y, width, height, startAngle, sweepAngle, + brush.Color.Hex, pen.Color.Hex, pen.Width, brush.Color.Opacity); else - { - Gfx.DrawImage(image, x, y, w.Value, h.Value); - } - } - this._drawingCtx.PushInstruction((oy) => InternalDrawImage(image, x, y+oy, ow, oh, cropImage), new XRect(x, y, w.Value, h.Value)); + canvas.StrokePie(x, y, width, height, startAngle, sweepAngle, + pen.Color.Hex, pen.Width, pen.Color.Opacity); + }); + _drawingCtx.PushInstruction(offset => InternalDrawPie(pen, brush, x, y + offset, width, height, startAngle, sweepAngle, isFilled), + new PdfRect(x, y, width, height), instrName: "DrawPie"); } - public void DrawPie(double x, double y, double? w, double? h, double startAngle, double sweepAngle, - bool isFilled) + public void DrawPolygon(IEnumerable points, bool isFilled) { - InternalDrawPie(x, y, w, h, startAngle, sweepAngle, isFilled, CurrentPen, CurrentBrush); + var page = CurrentPage; + var transformed = points.Select(point => new PdfPoint(ScaleX(point.X, page), ScaleY(point.Y, page))).ToArray(); + if (transformed.Length < 3) throw new ArgumentException("A polygon requires at least three points.", nameof(points)); + InternalDrawPolygon(ScalePen(CurrentPen, page), CurrentBrush, transformed, isFilled); } - private void InternalDrawPie(double x, double y, double? w, double? h, double startAngle, double sweepAngle, - bool isFilled, XPen pen, XBrush brush) + private void InternalDrawPolygon(PdfPen pen, PdfBrush brush, PdfPoint[] points, bool isFilled) { - if (isFilled) + var tuples = points.Select(point => (point.X, point.Y)).ToArray(); + AddCommand(canvas => canvas.Path(path => { - Gfx.DrawPie(pen, brush, x, y, w ?? 0, h ?? 0, startAngle, sweepAngle); - } - else - { - Gfx.DrawPie(pen, x, y, w ?? 0, h ?? 0, startAngle, sweepAngle); - } - this._drawingCtx.PushInstruction((oy) => InternalDrawPie(x, y+oy, w, h, startAngle, sweepAngle, isFilled, pen, brush), - new XRect(x, y, w ?? 0, h ?? 0)); + path.Polygon(tuples).Stroke(pen.Color.Hex, pen.Width).Opacity(pen.Color.Opacity); + if (isFilled) path.Fill(brush.Color.Hex).Opacity(brush.Color.Opacity); + })); + _drawingCtx.PushInstruction(offset => InternalDrawPolygon(pen, brush, points.Select(point => point.OffsetY(offset)).ToArray(), isFilled), points); } - public void DrawPolygon(IEnumerable points, bool isFilled) + public void BeginDrawRowTemplate(string name, int index, double offsetY, double newPageTopMargin) { - var ptArray = points.ToArray(); - InternalDrawPolygon(isFilled, ptArray, CurrentPen, CurrentBrush); + _drawingCtx.OpenBlock($"{name}:{index}", offsetY, newPageTopMargin); + _measurementStates.Push(_isMeasuring); + _isMeasuring = true; } - private void InternalDrawPolygon(bool isFilled, XPoint[] ptArray, XPen pen, XBrush brush) + public DrawingResult EndDrawRowTemplate(int index) { - if (isFilled) - { - Gfx.DrawPolygon(pen, brush, ptArray, XFillMode.Alternate); - } - else - { - Gfx.DrawPolygon(pen, ptArray); - } - this._drawingCtx.PushInstruction((oy) => InternalDrawPolygon(isFilled, ptArray.OffsetY(oy), pen, brush), ptArray); + var result = _drawingCtx.BlockRect; + var block = _drawingCtx.EndMeasure(); + _isMeasuring = _measurementStates.Pop(); + var pageOffsetY = 0d; + if (result.IsEmpty) result = new PdfRect(0, block.OffsetY, 0, 0); + else if (_drawingCtx.Level == 0) pageOffsetY = block.Draw(this, 0, 0); + _drawingCtx.CloseBlock(); + return new DrawingResult { DrawingRect = result, PageOffsetY = pageOffsetY }; } + public void BeginIterationTemplate(int rowCount) { } + public void EndIterationTemplate(double drawHeight) { } - public void BeginDrawRowTemplate(string name, int index, double offsetY, double newPageTopMargin) + public void RegisterOnNewPage(Action callback) { - //open virtual block - this._drawingCtx.OpenBlock($"{name}:{index}", offsetY, Gfx, newPageTopMargin); - //drawing is only to measure - _gfx = XGraphics.CreateMeasureContext(new XSize(PageWidth, PageHeight), - XGraphicsUnit.Point, XPageDirection.Downwards); + if (callback is not null && !_onNewPageHooks.Contains(callback)) _onNewPageHooks.Add(callback); } - public DrawingResult EndDrawRowTemplate(int index) + public void UnRegisterOnNewPage(Action callback) => _onNewPageHooks.Remove(callback); + + private void AddCommand(Action command) { - double newPageOffsetY = 0; - var result = this._drawingCtx.BlockRect; - InternalEndRowTemplate(index, result); - IInstructionBlock block; - var level = _drawingCtx.Level; - (block, _gfx) = this._drawingCtx.RestoreGraphics(); - if (result.IsEmpty) - { - return new() - { - DrawingRect = new XRect(0, block.OffsetY, 0, 0), - PageOffsetY = 0 - }; - } + if (!_isMeasuring) CurrentPage.Commands.Add(command); + } - if (level <= 1) - { - //draw only if rowTemplate if at root level - newPageOffsetY = block.Draw(this, 0, 0); - if (_logger?.IsEnabled(LogLevel.Debug) ?? false) - { - _logger.WriteDebug(this, $"EndDrawing block #{index} Rect={block.Rect}, newPageOffsetY={newPageOffsetY}"); - } - } + private static double MeasureText(string text, PdfFont font) + { + if (string.IsNullOrWhiteSpace(text)) return 0; + return VectorCanvas.MeasureTextWidth(text, font.Size, font.FamilyName, font.Style.HasFlag(PdfFontStyle.Bold), font.Style.HasFlag(PdfFontStyle.Italic)); + } - this._drawingCtx.CloseBlock(); + private static double MeasureCellWidth(string text, PdfFont font) => + text.Replace("\r\n", "\n").Split('\n').Select(line => MeasureText(line, font)).DefaultIfEmpty(0).Max(); - return new() - { - DrawingRect = result, - PageOffsetY = newPageOffsetY - }; - } + private static int MeasureCellLineCount(string text, double width, PdfFont font) => + WrapText(text, width, font).Count; - private void InternalEndRowTemplate(int index, XRect result) + private static List WrapText(string text, double? maxWidth, PdfFont font) { - if (_drawingCtx.DebugRowTemplate) + var lines = new List(); + foreach (var sourceLine in text.Replace("\r\n", "\n").Split('\n')) { - DebugRect(result); - Gfx.DrawLine(_debugPen, 0, 0, 5, 2); - Gfx.DrawLine(_debugPen, 0, 0, 2, 5); - Gfx.DrawLine(_debugPen, 0, 0, 10, 10); - DebugText($"{_drawingCtx.Level}.{index}", 10, 10); + if (maxWidth is null || maxWidth <= 0 || MeasureText(sourceLine, font) <= maxWidth) { lines.Add(sourceLine); continue; } + var current = string.Empty; + foreach (var word in sourceLine.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + var candidate = current.Length == 0 ? word : $"{current} {word}"; + if (current.Length > 0 && MeasureText(candidate, font) > maxWidth) { lines.Add(current); current = word; } + else current = candidate; + } + lines.Add(current); } - this._drawingCtx.PushInstruction((oy) => InternalEndRowTemplate(index, result.OffsetY(oy)), result, false, "EndRowTemplate"); + return lines; } - public void BeginIterationTemplate(int rowCount) - { - } + private static double ResolveX(double x, RecordedPage page) => x < 0 ? page.Width + x : x; + private static double ResolveY(double y, RecordedPage page) => y < 0 ? page.Height + y : y; + private static double ScaleX(double value, RecordedPage page) => value * page.ScaleX; + private static double ScaleY(double value, RecordedPage page) => value * page.ScaleY; - public void EndIterationTemplate(double drawHeight) + private static PdfPen ScalePen(PdfPen pen, RecordedPage page) { + var scaled = new PdfPen(pen.Color, pen.Width * ScaleFactor(page)) { DashStyle = pen.DashStyle }; + return scaled; } - private void DebugText(string text, double x, double y) - { - var fmt = new XStringFormat() - { - Alignment = XStringAlignment.Near, - LineAlignment = XLineAlignment.Near - }; + private static PdfFont ScaleFont(PdfFont font, RecordedPage page) => + new(font.FamilyName, font.Size * ScaleFactor(page), font.Style); - Gfx.DrawString(text, _debugFont.Value, XBrushes.Red, x, y, fmt); - } - private void DebugRect(XRect rect) - { - Gfx.DrawRectangle(_debugPen, rect); - } + private static double ScaleFactor(RecordedPage page) => + Math.Sqrt(page.ScaleX * page.ScaleY); - public void RegisterOnNewPage(Action callback) + private static (double Width, double Height) GetPageDimensions(PdfPageSize size, PdfPageOrientation orientation) { - if (callback != null && !_onNewPageHooks.Contains(callback)) + var dimensions = size switch { - _onNewPageHooks.Add(callback); - } + PdfPageSize.A0 => (2383.94, 3370.39), + PdfPageSize.A1 => (1683.78, 2383.94), + PdfPageSize.A2 => (1190.55, 1683.78), + PdfPageSize.A3 => (841.89, 1190.55), + PdfPageSize.A5 => (419.53, 595.28), + PdfPageSize.A6 => (297.64, 419.53), + PdfPageSize.Letter => (612d, 792d), + PdfPageSize.Legal => (612d, 1008d), + PdfPageSize.Ledger => (1224d, 792d), + PdfPageSize.Tabloid => (792d, 1224d), + _ => (595.28, 841.89), + }; + return orientation == PdfPageOrientation.Landscape ? (dimensions.Item2, dimensions.Item1) : dimensions; } - public void UnRegisterOnNewPage(Action callback) - { - _onNewPageHooks.Remove(callback); - } + public void Dispose() { } } -} \ No newline at end of file +} diff --git a/PdfSharpDslCore/Drawing/TableDefinition.cs b/PdfSharpDslCore/Drawing/TableDefinition.cs index b32ab07..e99609c 100644 --- a/PdfSharpDslCore/Drawing/TableDefinition.cs +++ b/PdfSharpDslCore/Drawing/TableDefinition.cs @@ -1,5 +1,3 @@ -using PdfSharpCore.Drawing; -using PdfSharpCore.Pdf; using System; using System.Collections.Generic; @@ -13,8 +11,8 @@ public class TableDefinition public double TopMarginOnPageBreak { get; set; } //header height, should be measure if not specified public double? HeaderHeight { get; set; } - public TrimMargins CellMargin { get; set; } = new TrimMargins() { All = 1 }; - public XBrush? HeaderBackColor { get; set; } + public PdfMargins CellMargin { get; set; } = new PdfMargins() { All = 1 }; + public PdfBrush? HeaderBackColor { get; set; } public List Rows { get; private set; } = new(); @@ -33,10 +31,10 @@ public double ColWidth(int i) /// public double ColMaxWidth(int i, double pageWidth) { - return Math.Max(0, Math.Min(Columns[i].MaxWidth ?? pageWidth, pageWidth)); + return Math.Min(Columns[i].MaxWidth ?? pageWidth, pageWidth); } - public XStringAlignment Alignment(int i) + public PdfHorizontalAlignment Alignment(int i) { return Columns[i].Alignment; } @@ -49,10 +47,10 @@ public class ColumnDefinition public double? DesiredWidth { get; set; } = null; public double? MaxWidth { get; set; } = null; - public XStringAlignment Alignment { get; set; } = XStringAlignment.Near; - public XFont? Font { get; set; } - public XBrush? Brush { get; set; } - public XBrush? BackColor { get; set; } + public PdfHorizontalAlignment Alignment { get; set; } = PdfHorizontalAlignment.Near; + public PdfFont? Font { get; set; } + public PdfBrush? Brush { get; set; } + public PdfBrush? BackColor { get; set; } public double DrawWidth { @@ -73,12 +71,12 @@ public class RowDefinition public double? DesiredHeight { get; set; } public double? MaxHeight { get; set; } + public List Cells { get; set; } = new(); + /// /// string because there is only draw text /// public string[] Data { get; set; } = Array.Empty(); - - public List Cells { get; set; } = new(); } public class CellDefinition @@ -86,7 +84,7 @@ public class CellDefinition public string Text { get; set; } = string.Empty; public int ColumnSpan { get; set; } = 1; public int RowSpan { get; set; } = 1; - public XStringAlignment? HorizontalAlignment { get; set; } - public XLineAlignment? VerticalAlignment { get; set; } + public PdfHorizontalAlignment? HorizontalAlignment { get; set; } + public PdfVerticalAlignment? VerticalAlignment { get; set; } } } \ No newline at end of file diff --git a/PdfSharpDslCore/Extensions/ParseTreeNodeExtensions.cs b/PdfSharpDslCore/Extensions/ParseTreeNodeExtensions.cs index f50c723..90fdb21 100644 --- a/PdfSharpDslCore/Extensions/ParseTreeNodeExtensions.cs +++ b/PdfSharpDslCore/Extensions/ParseTreeNodeExtensions.cs @@ -1,5 +1,5 @@ using Irony.Parsing; -using PdfSharpCore.Drawing; +using PdfSharpDslCore.Drawing; using System; using System.Collections.Generic; using System.Linq; @@ -41,22 +41,22 @@ public static IEnumerable ChildNodes(this ParseTreeNode node, str return node.ChildNodes.FirstOrDefault(n => n.Term != null && n.Term.Name == termName); } - public static XFontStyle ParseFontStyle(this ParseTreeNode? node) + public static PdfFontStyle ParseFontStyle(this ParseTreeNode? node) { if (node != null && node.Token != null) { var styleName = (string?)node.Token.Value; - if (Enum.TryParse(styleName, true, out var fontStyle)) + if (Enum.TryParse(styleName, true, out var fontStyle)) { return fontStyle; } } - return XFontStyle.Regular; + return PdfFontStyle.Regular; } - public static XColor ParseColor(this ParseTreeNode node) + public static PdfColor ParseColor(this ParseTreeNode node) { - var executor = (Func)(node.ChildNodes[0].Term.Name switch + var executor = (Func)(node.ChildNodes[0].Term.Name switch { "NamedColor" => ParseNamedColor, _ => ParseHexColor, @@ -64,36 +64,32 @@ public static XColor ParseColor(this ParseTreeNode node) return executor(node.ChildNodes[0]); } - private static XColor ParseNamedColor(ParseTreeNode node) + private static PdfColor ParseNamedColor(ParseTreeNode node) { var color = (string)node.ChildNodes[0].Token.Value; - - var staticColor = typeof(XColors) - .GetProperties(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(x => string.Compare(x.Name, color, StringComparison.OrdinalIgnoreCase) == 0); - return ((XColor?)staticColor?.GetValue(null)) ?? XColors.Black; + return PdfColors.FromName(color); } - private static XColor ParseHexColor(ParseTreeNode node) + private static PdfColor ParseHexColor(ParseTreeNode node) { var colorValue = node.ChildNodes[0].Token.Value; if (colorValue is double) { - return XColor.FromGrayScale(Convert.ToDouble(colorValue)); + return PdfColor.FromGrayScale(Convert.ToDouble(colorValue)); } else { if (node.ChildNodes[0].Token.Length == 8) { uint argb = ((uint)0xff000000) | Convert.ToUInt32(colorValue); - return XColor.FromArgb(argb); + return PdfColor.FromArgb(argb); } else if (node.ChildNodes[0].Token.Length == 10) { - int argb = Convert.ToInt32(colorValue); - return XColor.FromArgb(argb); + uint argb = unchecked((uint)Convert.ToInt32(colorValue)); + return PdfColor.FromArgb(argb); } - return XColor.FromArgb(Convert.ToInt32(colorValue)); + return PdfColor.FromArgb(unchecked((uint)Convert.ToInt32(colorValue))); } } diff --git a/PdfSharpDslCore/Extensions/XRectExtensions.cs b/PdfSharpDslCore/Extensions/XRectExtensions.cs index 36e555f..5a38d8f 100644 --- a/PdfSharpDslCore/Extensions/XRectExtensions.cs +++ b/PdfSharpDslCore/Extensions/XRectExtensions.cs @@ -1,29 +1,27 @@ using System.Collections.Generic; -using PdfSharpCore.Drawing; +using PdfSharpDslCore.Drawing; namespace PdfSharpDslCore.Extensions { - public static class XRectExtensions + public static class PdfRectExtensions { - public static XRect OffsetY(this XRect r, double y) + public static PdfRect OffsetY(this PdfRect r, double y) { var result = r; result.Offset(0, y); return result; } - public static XPoint OffsetY(this XPoint p, double y) + public static PdfPoint OffsetY(this PdfPoint p, double y) { - var result = p; - result.Offset(0,y); - return result; + return p.OffsetY(y); } - public static XPoint[] OffsetY(this XPoint[] pts, double y) + public static PdfPoint[] OffsetY(this PdfPoint[] pts, double y) { if (y != 0) { - List resultPts = new List(); + List resultPts = new List(); foreach (var pt in pts) { resultPts.Add(pt.OffsetY(y)); diff --git a/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs b/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs index bd70d7f..12ce808 100644 --- a/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs +++ b/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs @@ -1,6 +1,4 @@ using Irony.Parsing; -using PdfSharpCore; -using PdfSharpCore.Drawing; using PdfSharpDslCore.Drawing; using PdfSharpDslCore.Evaluation; using PdfSharpDslCore.Extensions; @@ -84,13 +82,13 @@ protected override void ExecutePen(IPdfDocumentDrawer state, ParseTreeNode width { var width = EvaluateForDouble(widthNode) ?? 0; var color = colorNode.ParseColor(); - XDashStyle style = XDashStyle.Solid; + PdfDashStyle style = PdfDashStyle.Solid; if (styleNode != null) { - Enum.TryParse(styleNode.Token.ValueString, true, out style); + Enum.TryParse(styleNode.Token.ValueString, true, out style); } - var pen = new XPen(color, width) + var pen = new PdfPen(color, width) { DashStyle = style }; @@ -100,7 +98,7 @@ protected override void ExecutePen(IPdfDocumentDrawer state, ParseTreeNode width protected override void ExecuteHBrush(IPdfDocumentDrawer drawer, ParseTreeNode colorNode) { var color = colorNode.ParseColor(); - drawer.HighlightBrush = color.A == 0 ? null : new XSolidBrush(color); + drawer.HighlightBrush = color.A == 0 ? null : new PdfBrush(color); } protected override void ExecuteFont(IPdfDocumentDrawer drawer, ParseTreeNode fontNode) @@ -114,15 +112,15 @@ protected override void ExecuteNewPage(IPdfDocumentDrawer drawer, { var nSize = sizeNode; var nOrientation = orientationNode; - PageSize? pageSize = null; - if (nSize != null && Enum.TryParse(nSize.Token.Text, out var size)) + PdfPageSize? pageSize = null; + if (nSize != null && Enum.TryParse(nSize.Token.Text, out var size)) { pageSize = size; } - PageOrientation? pageOrientation = null; + PdfPageOrientation? pageOrientation = null; if (nOrientation != null && - Enum.TryParse(nOrientation.Token.Text, true, out var orientation)) + Enum.TryParse(nOrientation.Token.Text, true, out var orientation)) { pageOrientation = orientation; } @@ -169,7 +167,7 @@ protected override void ExecuteTitle(IPdfDocumentDrawer drawer, ParseTreeNode ma ParseTreeNode alignmentsNode, ParseTreeNode contentNode) { - var text = Convert.ToString(EvaluateForObject(contentNode)); + var text = Convert.ToString(EvaluateForObject(contentNode)) ?? string.Empty; var margin = ParseMargin(marginNode); var (hAlign, vAlign) = ParseTextAlignment(alignmentsNode); @@ -179,11 +177,11 @@ protected override void ExecuteTitle(IPdfDocumentDrawer drawer, ParseTreeNode ma protected override void ExecutePolygon(IPdfDocumentDrawer state, IEnumerable pointNodes, bool isFilled) { - var points = new List(); + var points = new List(); foreach (var ptNode in pointNodes) { var (x, y) = ParsePointLocation(ptNode); - points.Add(new XPoint(x, y)); + points.Add(new PdfPoint(x, y)); } state.DrawPolygon(points, isFilled); @@ -298,7 +296,7 @@ protected override void ExecuteImage(IPdfDocumentDrawer drawer, ParseTreeNode lo string.Equals(x.Token?.Text, "crop", StringComparison.OrdinalIgnoreCase)) == true; } - XImage image; + PdfImage image; if (isEmbedded && !string.IsNullOrWhiteSpace(imagePath)) { if (imagePath.StartsWith("data:image")) @@ -306,8 +304,7 @@ protected override void ExecuteImage(IPdfDocumentDrawer drawer, ParseTreeNode lo imagePath = imagePath.Split(',')[1]; } - using var stream = new MemoryStream(System.Convert.FromBase64String(imagePath)); - image = XImage.FromStream(() => stream); + image = new PdfImage(System.Convert.FromBase64String(imagePath)); } else { @@ -316,13 +313,10 @@ protected override void ExecuteImage(IPdfDocumentDrawer drawer, ParseTreeNode lo imagePath = Path.Combine(this.BaseDirectory, imagePath); } - image = XImage.FromFile(imagePath); + image = new PdfImage(File.ReadAllBytes(imagePath)); } - using (image) - { - drawer.DrawImage(image, x, y, w, h, unit == "pixel", crop); - } + drawer.DrawImage(image, x, y, w, h, unit == "pixel", crop); } protected override void ExecuteUdfInvokeStatement(IPdfDocumentDrawer state, string fnName, @@ -393,7 +387,7 @@ protected virtual void UdfCall(IPdfDocumentDrawer drawer, string udfName, string protected override void ExecuteBrush(IPdfDocumentDrawer state, ParseTreeNode colorNode) { var color = colorNode.ParseColor(); - state.CurrentBrush = new XSolidBrush(color); + state.CurrentBrush = new PdfBrush(color); } protected override void ExecuteRowTemplateStatement(IPdfDocumentDrawer state, @@ -539,7 +533,7 @@ private TableDefinition GenerateTableDefinition(ParseTreeNode node) if (headStyle != null && headStyle.ChildNodes.Count > 0) { var color = headStyle.ChildNodes[0].ParseColor(); - result.HeaderBackColor = new XSolidBrush(color); + result.HeaderBackColor = new PdfBrush(color); } GenerateTableHead(node.ChildNodes("TableHeadCol"), result); @@ -615,6 +609,34 @@ private void GenerateTableRows(IEnumerable nodes, TableDefinition } } + private CellDefinition ParseTableCell(ParseTreeNode node) + { + var expression = node.ChildNodes.Last(); + var cell = new CellDefinition + { + Text = EvaluateForObject(expression)?.ToString() ?? string.Empty, + ColumnSpan = ParseTableCellSpan(node.ChildNode("TableCellColSpan")), + RowSpan = ParseTableCellSpan(node.ChildNode("TableCellRowSpan")) + }; + var alignment = node.ChildNode("TextAlignment"); + if (alignment is not null) + { + var hNode = alignment.ChildNode("HAlign"); + var vNode = alignment.ChildNode("VAlign"); + if (hNode?.ChildNodes.Count > 0) + cell.HorizontalAlignment = ParseTextAlignment(hNode, null).Item1; + if (vNode?.ChildNodes.Count > 0) + cell.VerticalAlignment = ParseTextAlignment(null, vNode).Item2; + } + return cell; + } + + private int ParseTableCellSpan(ParseTreeNode? node) + { + if (node is null || node.ChildNodes.Count == 0) return 1; + return Math.Max(1, Convert.ToInt32(EvaluateForDouble(node.ChildNodes.Last()))); + } + private void GenerateTableHead(IEnumerable nodes, TableDefinition tbl) { foreach (var col in nodes) @@ -640,8 +662,8 @@ private void GenerateTableHead(IEnumerable nodes, TableDefinition var colors = col.ChildNode("TableColColors"); if (colors?.ChildNodes.Count > 0) { - colDef.Brush = new XSolidBrush(colors.ChildNodes[0].ParseColor()); - colDef.BackColor = new XSolidBrush(colors.ChildNodes[1].ParseColor()); + colDef.Brush = new PdfBrush(colors.ChildNodes[0].ParseColor()); + colDef.BackColor = new PdfBrush(colors.ChildNodes[1].ParseColor()); } //name @@ -651,45 +673,17 @@ private void GenerateTableHead(IEnumerable nodes, TableDefinition } } - private CellDefinition ParseTableCell(ParseTreeNode node) - { - var cell = new CellDefinition - { - Text = EvaluateForObject(node.ChildNodes.Last())?.ToString() ?? string.Empty, - ColumnSpan = ParseTableCellSpan(node.ChildNode("TableCellColSpan")), - RowSpan = ParseTableCellSpan(node.ChildNode("TableCellRowSpan")) - }; - - var alignment = node.ChildNode("TextAlignment"); - if (alignment is not null) - { - var hNode = alignment.ChildNode("HAlign"); - var vNode = alignment.ChildNode("VAlign"); - var (hAlign, vAlign) = ParseTextAlignment(hNode, vNode); - if (hNode?.ChildNodes.Count > 2) cell.HorizontalAlignment = hAlign; - if (vNode?.ChildNodes.Count > 2) cell.VerticalAlignment = vAlign; - } - - return cell; - } - - private int ParseTableCellSpan(ParseTreeNode? node) - { - if (node is null || node.ChildNodes.Count == 0) return 1; - return Math.Max(1, Convert.ToInt32(EvaluateForDouble(node.ChildNodes.Last()))); - } - - private static (XStringAlignment, XLineAlignment) ParseTextAlignment(ParseTreeNode alignNode) + private static (PdfHorizontalAlignment, PdfVerticalAlignment) ParseTextAlignment(ParseTreeNode alignNode) { var hNode = alignNode.Term.Name == "HAlign" ? alignNode : null; var vNode = alignNode.Term.Name == "VAlign" ? alignNode : null; return ParseTextAlignment(hNode, vNode); } - private static (XStringAlignment, XLineAlignment) ParseTextAlignment(ParseTreeNode? hNode, ParseTreeNode? vNode) + private static (PdfHorizontalAlignment, PdfVerticalAlignment) ParseTextAlignment(ParseTreeNode? hNode, ParseTreeNode? vNode) { - var hAlign = XStringAlignment.Near; - var vAlign = XLineAlignment.Near; + var hAlign = PdfHorizontalAlignment.Near; + var vAlign = PdfVerticalAlignment.Near; if (hNode != null && hNode.ChildNodes.Count > 2) { switch (hNode.ChildNodes[2].Token.Value) @@ -697,10 +691,10 @@ private static (XStringAlignment, XLineAlignment) ParseTextAlignment(ParseTreeNo case "left": break; case "hcenter": - hAlign = XStringAlignment.Center; + hAlign = PdfHorizontalAlignment.Center; break; case "right": - hAlign = XStringAlignment.Far; + hAlign = PdfHorizontalAlignment.Far; break; } } @@ -711,10 +705,10 @@ private static (XStringAlignment, XLineAlignment) ParseTextAlignment(ParseTreeNo case "top": break; case "vcenter": - vAlign = XLineAlignment.Center; + vAlign = PdfVerticalAlignment.Center; break; case "bottom": - vAlign = XLineAlignment.Far; + vAlign = PdfVerticalAlignment.Far; break; } @@ -796,7 +790,7 @@ private string InternalSetVar(ParseTreeNode node) return InternalSetVar(node.ChildNodes[1], node.ChildNodes[3]); } - private XFont ExtractFont(ParseTreeNode node) + private PdfFont ExtractFont(ParseTreeNode node) { ParseTreeNode styleNode = null!; string fontName = string.Empty; @@ -815,7 +809,7 @@ private XFont ExtractFont(ParseTreeNode node) } var style = styleNode.ParseFontStyle(); - return new XFont(fontName, fontSize, style, XPdfFontOptions.UnicodeDefault); + return new PdfFont(fontName, fontSize, style); } } } \ No newline at end of file diff --git a/PdfSharpDslCore/Parser/PdfGrammar.cs b/PdfSharpDslCore/Parser/PdfGrammar.cs index 35aeda7..7c15cb8 100644 --- a/PdfSharpDslCore/Parser/PdfGrammar.cs +++ b/PdfSharpDslCore/Parser/PdfGrammar.cs @@ -1,8 +1,7 @@ using Irony; using Irony.Parsing; -using PdfSharpCore; -using PdfSharpCore.Drawing; +using PdfSharpDslCore.Drawing; using System; using System.Collections.Generic; using System.Data; @@ -113,8 +112,6 @@ public PdfGrammar() var TableRowListOrRowTemplate = new NonTerminal("TableRowListOrRowTemplate"); var TableRowTemplate = new NonTerminal("TableRowTemplate"); var TableRowTemplateCount = new NonTerminal("TableRowTemplateCount"); - var TableCellColSpan = new NonTerminal("TableCellColSpan"); - var TableCellRowSpan = new NonTerminal("TableCellRowSpan"); var PointAutoLocation = new NonTerminal("PointAutoLocation"); var NumberOrAuto = new NonTerminal("NumberOrAuto"); @@ -260,9 +257,9 @@ public PdfGrammar() VarSmt.Rule = ToTerm("VAR") + variableLiteral + "=" + FormulaExpression + semi; ColorExp.Rule = NamedColor | HexColor; - foreach (var prop in typeof(XColors).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + foreach (var colorName in PdfColors.Names) { - var name = prop.Name.ToLowerInvariant(); + var name = colorName.ToLowerInvariant(); if (NamedColor.Rule == null) { NamedColor.Rule = ToTerm(name, $"color-{name}"); @@ -276,14 +273,14 @@ public PdfGrammar() HexColor.Rule = colorNumber; styleExpr.Rule = Empty; - foreach (var enumName in Enum.GetNames(typeof(XFontStyle))) + foreach (var enumName in Enum.GetNames(typeof(PdfFontStyle))) { var styleName = enumName.ToLowerInvariant(); styleExpr.Rule |= ToTerm(styleName, $"style-{styleName}"); } - //TextAlignment is not yet supported on multiline text (only top left is provided by pdfsharpcore) + //Multiline alignment is implemented by the TerraPDF canvas adapter. //multiline TextSmt.Rule = ToInstructionTerm("TEXT") + RectOrPointLocation + OptArg("MaxWidth", FormulaExpression) + Arg("Text") + FormulaExpression; @@ -305,7 +302,7 @@ public PdfGrammar() PageSize.Rule = Empty; - var names = Enum.GetNames(typeof(PageSize)); + var names = Enum.GetNames(typeof(PdfPageSize)); var firstSize = names.First(); PageSize.Rule |= ToTerm(firstSize, $"pagesize-{firstSize}"); foreach (var prop in names.Skip(1)) @@ -335,9 +332,7 @@ public PdfGrammar() TableColWidth.Rule = Arg("Width") + NumberOrAuto + Arg("MaxWidth") + NumberOrAuto; TableColList.Rule = MakeStarRule(TableColList, TableCol); TableRow.Rule = ToTerm("ROW") + TableRowStyle + TableColList + ToTerm("ENDROW"); - TableCellColSpan.Rule = Empty | Arg("ColSpan") + number_literal; - TableCellRowSpan.Rule = Empty | Arg("RowSpan") + number_literal; - TableCol.Rule = ToTerm("COL") + TableCellColSpan + TableCellRowSpan + TextAlignment + FormulaExpression + semi; + TableCol.Rule = ToTerm("COL") + FormulaExpression + semi; TableLocation.Rule = PointLocation /*+ "," + PointAutoLocation*/; PointAutoLocation.Rule = NumberOrAuto + "," + NumberOrAuto; NumberOrAuto.Rule = FormulaExpression | "auto"; diff --git a/PdfSharpDslCore/PdfSharpDslCore.csproj b/PdfSharpDslCore/PdfSharpDslCore.csproj index a1e2aa1..834cf38 100644 --- a/PdfSharpDslCore/PdfSharpDslCore.csproj +++ b/PdfSharpDslCore/PdfSharpDslCore.csproj @@ -1,43 +1,42 @@ - + - netstandard2.0 + $(NetLibTfms) Readme.md - 9.0 - enable Generate PDF using DSL PdfSharpDslCore - 1.0.6 - Pierrick Gourlain - - A DSL using PdfSharpCore to generate PDF - https://github.com/pgourlain/bnf_and_pdf + true + A DSL using TerraPDF to generate PDF Icon.jpg - LICENSE.md - - True - \ - + + + + + + - - - - - + - - True - \ - - - True - \ - + + + + + + + + + + diff --git a/PdfSharpDslCore/Readme.md b/PdfSharpDslCore/Readme.md index f4ffd0b..ef67f90 100644 --- a/PdfSharpDslCore/Readme.md +++ b/PdfSharpDslCore/Readme.md @@ -1,9 +1,9 @@  # Introduction -Package to print PDF using a specific DSL, using Irony.Net and PdfSharpCore +Package to print PDF using a specific DSL, using Irony.Net and TerraPDF. -The reusable library targets `netstandard2.0`. The source generator package also targets `netstandard2.0` and can be consumed by a `net10.0` application. The repository's test and console projects target `net10.0`. +The renderer multi-targets `net8.0` and `net10.0`. `PdfSharpDsl.Language` and the source generator target `netstandard2.0` and can be consumed by modern .NET applications. The repository enforces at least 90% line coverage for `PdfSharpDslCore` through `scripts\coverage.ps1`. diff --git a/README.md b/README.md index 59bb7b8..76e2742 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,14 @@ [![NuGet Version](https://img.shields.io/nuget/v/PdfSharpDslCore.svg)](https://www.nuget.org/packages/PdfSharpDslCore/) [![CI](https://github.com/pgourlain/bnf_and_pdf/actions/workflows/build.yml/badge.svg)](https://github.com/pgourlain/bnf_and_pdf/actions/workflows/build.yml) -This is a sample library that use [Irony.Net](https://github.com/IronyProject/Irony) to define a grammar to print PDF using [PdfSharpCore](https://github.com/ststeiger/PdfSharpCore/) +This is a sample library that uses [Irony.Net](https://github.com/IronyProject/Irony) to define a grammar and [TerraPDF](https://www.nuget.org/packages/TerraPDF) to print PDF. ## Current support -- `PdfSharpDslCore` and `PdfSharpDslCore.Generator` target `netstandard2.0`. +- `PdfSharpDslCore` multi-targets `net8.0` and `net10.0`; `PdfSharpDsl.Language` and `PdfSharpDslCore.Generator` target `netstandard2.0` because they are loaded by the compiler. - `PdfSharpDslConsole` and the test project target `net10.0`. - The repository is pinned to SDK `10.0.400` in `global.json`. +- Version, licence, package metadata and every NuGet version are centralized in [`_build/`](_build/): `Version.props` (the single product version), `Common.props` (shared metadata and target-framework aliases) and `Packages.props` (central package management). The `Directory.Build.props`, `Directory.Build.targets` and `Directory.Packages.props` files at the repository root only import these. Change a version or a target framework there, never in an individual `.csproj`. - The generator package includes its analyzer dependencies and supports clean NuGet consumer builds. Run the full test and core coverage gate with: @@ -40,12 +41,9 @@ if (parsingResult.HasErrors()) } else { - //PdfSharpCore cclasses - var document = new PdfDocument(); - //draw parsing result - using var drawer = new PdfDocumentDrawer(document); + using var drawer = new PdfDocumentDrawer(); new PdfDrawerVisitor().Draw(drawer, parsingResult); - document.Save("helloworld.pdf"); + drawer.PublishPdf("helloworld.pdf"); } ``` @@ -61,14 +59,14 @@ sequenceDiagram participant yourprogram participant PdfSharpDslCore participant Irony - participant PdfSharp + participant TerraPDF yourprogram->>Irony: Parse file or text. Irony -->> PdfSharpDslCore: use PdfGrammar. Irony-->>yourprogram: Parsing result. yourprogram->>PdfSharpDslCore: Define callback for Formula functions. yourprogram->>PdfSharpDslCore: Draw() - PdfSharpDslCore -->>PdfSharp: use Document to draw. + PdfSharpDslCore -->>TerraPDF: publish recorded canvas commands. PdfSharpDslCore->>yourprogram: call registered formula functions. yourprogram-->>PdfSharpDslCore: function result. PdfSharpDslCore->>PdfSharpDslCore: execute all instructions from source file. @@ -573,7 +571,7 @@ yellowgreen this package is build on top of - PDF : - - pdfSharpCore : https://github.com/ststeiger/PdfSharpCore + - TerraPDF : https://www.nuget.org/packages/TerraPDF - Parsers : - Irony : https://github.com/IronyProject/Irony diff --git a/_build/Common.props b/_build/Common.props new file mode 100644 index 0000000..b9b4ba7 --- /dev/null +++ b/_build/Common.props @@ -0,0 +1,44 @@ + + + + + + net8.0;net10.0 + + net10.0 + + netstandard2.0 + + + + + latest + enable + + + + + Pierrick Gourlain + Pierrick Gourlain + Copyright (c) 2022-2026 Pierrick Gourlain + https://github.com/pgourlain/bnf_and_pdf + https://github.com/pgourlain/bnf_and_pdf + git + pdf;dsl;irony;terrapdf;pdf-generation;source-generator + LICENSE.md + + + + + true + true + snupkg + true + + + + + diff --git a/_build/Common.targets b/_build/Common.targets new file mode 100644 index 0000000..c1d6e77 --- /dev/null +++ b/_build/Common.targets @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/_build/Packages.props b/_build/Packages.props new file mode 100644 index 0000000..d680db6 --- /dev/null +++ b/_build/Packages.props @@ -0,0 +1,33 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_build/Version.props b/_build/Version.props new file mode 100644 index 0000000..cf354fc --- /dev/null +++ b/_build/Version.props @@ -0,0 +1,13 @@ + + + + + 2.0.0 + + + + diff --git a/bnf_and_pdf.sln b/bnf_and_pdf.sln index b1200b8..cb64dd7 100644 --- a/bnf_and_pdf.sln +++ b/bnf_and_pdf.sln @@ -13,33 +13,91 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .gitignore = .gitignore CHANGELOG.md = CHANGELOG.md + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + global.json = global.json README.md = README.md + _build\Common.props = _build\Common.props + _build\Common.targets = _build\Common.targets + _build\Packages.props = _build\Packages.props + _build\Version.props = _build\Version.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PdfSharpDslCore.Generator", "PdfSharpDslCore.Generator\PdfSharpDslCore.Generator.csproj", "{A6092AA9-EC00-4C4F-AF43-E039E400CC3B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PdfSharpDsl.Language", "PdfSharpDsl.Language\PdfSharpDsl.Language.csproj", "{2373085B-7611-43B6-A4A1-F2DE608817C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Debug|Any CPU.Build.0 = Debug|Any CPU + {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Debug|x64.ActiveCfg = Debug|Any CPU + {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Debug|x64.Build.0 = Debug|Any CPU + {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Debug|x86.ActiveCfg = Debug|Any CPU + {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Debug|x86.Build.0 = Debug|Any CPU {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Release|Any CPU.ActiveCfg = Release|Any CPU {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Release|Any CPU.Build.0 = Release|Any CPU + {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Release|x64.ActiveCfg = Release|Any CPU + {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Release|x64.Build.0 = Release|Any CPU + {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Release|x86.ActiveCfg = Release|Any CPU + {599099A9-C23A-4EE3-A3BA-D65E95E52014}.Release|x86.Build.0 = Release|Any CPU {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Debug|x64.Build.0 = Debug|Any CPU + {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Debug|x86.Build.0 = Debug|Any CPU {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Release|Any CPU.Build.0 = Release|Any CPU + {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Release|x64.ActiveCfg = Release|Any CPU + {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Release|x64.Build.0 = Release|Any CPU + {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Release|x86.ActiveCfg = Release|Any CPU + {31F6FA1A-55D5-45C1-8AB8-8ED23984BA3A}.Release|x86.Build.0 = Release|Any CPU {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Debug|x64.ActiveCfg = Debug|Any CPU + {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Debug|x64.Build.0 = Debug|Any CPU + {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Debug|x86.Build.0 = Debug|Any CPU {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Release|Any CPU.Build.0 = Release|Any CPU + {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Release|x64.ActiveCfg = Release|Any CPU + {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Release|x64.Build.0 = Release|Any CPU + {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Release|x86.ActiveCfg = Release|Any CPU + {D737D13F-8593-4AB1-961B-7D5F736ACF2E}.Release|x86.Build.0 = Release|Any CPU {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Debug|x64.Build.0 = Debug|Any CPU + {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Debug|x86.Build.0 = Debug|Any CPU {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Release|Any CPU.Build.0 = Release|Any CPU + {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Release|x64.ActiveCfg = Release|Any CPU + {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Release|x64.Build.0 = Release|Any CPU + {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Release|x86.ActiveCfg = Release|Any CPU + {A6092AA9-EC00-4C4F-AF43-E039E400CC3B}.Release|x86.Build.0 = Release|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Debug|x64.Build.0 = Debug|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Debug|x86.Build.0 = Debug|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Release|Any CPU.Build.0 = Release|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Release|x64.ActiveCfg = Release|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Release|x64.Build.0 = Release|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Release|x86.ActiveCfg = Release|Any CPU + {2373085B-7611-43B6-A4A1-F2DE608817C4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/pdfsharpdslTests/DrawingValueTests.cs b/pdfsharpdslTests/DrawingValueTests.cs index 740684b..bf0af5a 100644 --- a/pdfsharpdslTests/DrawingValueTests.cs +++ b/pdfsharpdslTests/DrawingValueTests.cs @@ -1,4 +1,3 @@ -using PdfSharpCore.Drawing; using PdfSharpDslCore.Drawing; using PdfSharpDslCore.Extensions; @@ -9,8 +8,8 @@ public class DrawingValueTests [Fact] public void OffsetYMovesRectanglesAndPointsWithoutChangingInputs() { - var rectangle = new XRect(10, 20, 30, 40); - var point = new XPoint(5, 6); + var rectangle = new PdfRect(10, 20, 30, 40); + var point = new PdfPoint(5, 6); var movedRectangle = rectangle.OffsetY(7); var movedPoint = point.OffsetY(8); @@ -26,7 +25,7 @@ public void OffsetYMovesRectanglesAndPointsWithoutChangingInputs() [Fact] public void OffsetYMovesPointArraysOnlyWhenNeeded() { - var points = new[] { new XPoint(1, 2), new XPoint(3, 4) }; + var points = new[] { new PdfPoint(1, 2), new PdfPoint(3, 4) }; var unchanged = points.OffsetY(0); var moved = points.OffsetY(10); @@ -50,7 +49,7 @@ public void TableDefinitionCalculatesColumnDimensionsAndAlignment() { DesiredWidth = 80, MaxWidth = 50, - Alignment = XStringAlignment.Center + Alignment = PdfHorizontalAlignment.Center }); table.Columns.Add(new ColumnDefinition()); table.Rows.Add(new RowDefinition @@ -64,7 +63,7 @@ public void TableDefinitionCalculatesColumnDimensionsAndAlignment() Assert.Equal(0, table.ColWidth(1)); Assert.Equal(50, table.ColMaxWidth(0, 100)); Assert.Equal(40, table.ColMaxWidth(1, 40)); - Assert.Equal(XStringAlignment.Center, table.Alignment(0)); + Assert.Equal(PdfHorizontalAlignment.Center, table.Alignment(0)); Assert.Equal(50, table.Columns[0].DrawWidth); Assert.Equal(0, table.Columns[1].DrawWidth); Assert.Equal(12, table.TopMarginOnPageBreak); @@ -78,7 +77,7 @@ public void TableDefinitionCalculatesColumnDimensionsAndAlignment() [Fact] public void DrawingResultStoresRectangleAndPageOffset() { - var rectangle = new XRect(1, 2, 3, 4); + var rectangle = new PdfRect(1, 2, 3, 4); var result = new DrawingResult { DrawingRect = rectangle, @@ -92,28 +91,23 @@ public void DrawingResultStoresRectangleAndPageOffset() [Fact] public void TextRectangleAlignmentUsesRelativeBounds() { - var bounds = new XRect(10, 20, 100, 50); - var textSize = new XSize(30, 10); - var centered = DrawingHelper.RectFromStringFormat(bounds, textSize, new XStringFormat - { - Alignment = XStringAlignment.Center, - LineAlignment = XLineAlignment.Center - }); - var far = DrawingHelper.RectFromStringFormat(bounds, textSize, new XStringFormat - { - Alignment = XStringAlignment.Far, - LineAlignment = XLineAlignment.Far - }); + var bounds = new PdfRect(10, 20, 100, 50); + var textSize = new PdfSize(30, 10); + var centered = DrawingHelper.RectFromStringFormat(bounds, textSize, + PdfHorizontalAlignment.Center, PdfVerticalAlignment.Center); + var far = DrawingHelper.RectFromStringFormat(bounds, textSize, + PdfHorizontalAlignment.Far, PdfVerticalAlignment.Far); - Assert.Equal(new XRect(45, 40, 30, 10), centered); - Assert.Equal(new XRect(80, 60, 30, 10), far); + Assert.Equal(new PdfRect(45, 40, 30, 10), centered); + Assert.Equal(new PdfRect(80, 60, 30, 10), far); } [Fact] public void TextRectangleIsClippedToProvidedBounds() { - var bounds = new XRect(10, 20, 100, 50); - var result = DrawingHelper.RectFromStringFormat(bounds, new XSize(200, 100), XStringFormats.TopLeft); + var bounds = new PdfRect(10, 20, 100, 50); + var result = DrawingHelper.RectFromStringFormat(bounds, new PdfSize(200, 100), + PdfHorizontalAlignment.Near, PdfVerticalAlignment.Near); Assert.Equal(bounds, result); } diff --git a/pdfsharpdslTests/GenerationBaseTests.cs b/pdfsharpdslTests/GenerationBaseTests.cs index f8e352b..07e9761 100644 --- a/pdfsharpdslTests/GenerationBaseTests.cs +++ b/pdfsharpdslTests/GenerationBaseTests.cs @@ -1,5 +1,4 @@ -using PdfSharpCore.Pdf; -using PdfSharpDslCore.Drawing; +using PdfSharpDslCore.Drawing; using PdfSharpDslCore.Parser; using System; using System.Collections.Generic; @@ -17,14 +16,12 @@ protected MemoryStream GeneratePdf(string dslFileContent) { var parsingResult = ParseText(dslFileContent); - //PdfSharpCore cclasses - using var document = new PdfDocument(); - //draw parsing result - using var drawer = new PdfDocumentDrawer(document); + using var drawer = new PdfDocumentDrawer(); new PdfDrawerVisitor().Draw(drawer, parsingResult); var result = new MemoryStream(); - document.Save(result, false); + drawer.PublishPdf(result); + result.Position = 0; return result; } } diff --git a/pdfsharpdslTests/GenerationTableTests.cs b/pdfsharpdslTests/GenerationTableTests.cs index 4bd2112..98d5b1d 100644 --- a/pdfsharpdslTests/GenerationTableTests.cs +++ b/pdfsharpdslTests/GenerationTableTests.cs @@ -1,6 +1,4 @@ -using PdfSharpCore.Pdf.IO; -using PdfSharpCore.Pdf; -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -14,15 +12,15 @@ public class GenerationTableTests : GenerationBaseTests { [Theory] [InlineData("pdf1-table.txt")] - [InlineData("pdf1-table-merged.txt")] public void TestDrawingNotFailed(string file) { var input = File.ReadAllText($"./ValidInputFiles/{file}"); using var memStm = GeneratePdf(input); - memStm.Position = 0; - using PdfDocument pdfDocument = PdfReader.Open(memStm, PdfDocumentOpenMode.Import); - //generation and import not failed - Assert.True(true); + var pdf = new PdfBinaryInspector(memStm); + + Assert.True(pdf.HasPdfHeader); + Assert.True(pdf.PageCount > 0); + Assert.True(pdf.ContentStreamCount > 0); } } } diff --git a/pdfsharpdslTests/GenerationTests.cs b/pdfsharpdslTests/GenerationTests.cs index 9ffc516..ec8d0c7 100644 --- a/pdfsharpdslTests/GenerationTests.cs +++ b/pdfsharpdslTests/GenerationTests.cs @@ -1,10 +1,6 @@ -using PdfSharpCore.Pdf.IO; -using PdfSharpCore.Pdf; using PdfSharpDslCore.Parser; using PdfSharpDslCore.Drawing; -using PdfSharpCore.Pdf.Content; -using PdfSharpCore.Pdf.Content.Objects; using System.Diagnostics; using System.Text; using System.Diagnostics.CodeAnalysis; @@ -21,10 +17,10 @@ public void TestDrawingNotFailed(string file) { var input = File.ReadAllText($"./ValidInputFiles/{file}"); using var memStm = GeneratePdf(input); - memStm.Position = 0; - using PdfDocument pdfDocument = PdfReader.Open(memStm, PdfDocumentOpenMode.Import); - //generation and import not failed - Assert.True(true); + var pdf = new PdfBinaryInspector(memStm); + + Assert.True(pdf.HasPdfHeader); + Assert.True(pdf.PageCount > 0); } @@ -34,17 +30,24 @@ public void TestdrawingLinesOutPut(string file) { var input = File.ReadAllText($"./ValidInputFiles/{file}"); using var memStm = GeneratePdf(input); - memStm.Position = 0; - //reopne pdf to check if "print" works - using PdfDocument pdfDocument = PdfReader.Open(memStm, PdfDocumentOpenMode.Import); - Assert.Equal(1, pdfDocument.PageCount); - var p = pdfDocument.Pages[0]; - var h = p.Height.Point; - Assert.NotNull(p); + var pdf = new PdfBinaryInspector(memStm); + + Assert.True(pdf.HasPdfHeader); + Assert.Equal(1, pdf.PageCount); + Assert.True(pdf.ContentStreamCount > 0); + } + + [Fact] + public void DrawingEmbeddedImageAddsPdfImageResource() + { + const string input = "NEWPAGE A4 portrait;" + + "IMAGE 10,10,20,20 point fit Data=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=\";"; + + using var memStm = GeneratePdf(input); + var pdf = new PdfBinaryInspector(memStm); - var lines = ExtractLines(p).ToArray(); - Assert.Equal(4, lines.Length); - //TODO check values extracted from lines + Assert.True(pdf.HasPdfHeader); + Assert.Equal(1, pdf.ImageCount); } [Theory()] @@ -53,12 +56,10 @@ public void TestdrawingUdfsOutPut(string file) { var input = File.ReadAllText($"./ValidInputFiles/{file}"); using var memStm = GeneratePdf(input); - memStm.Position = 0; - //reopen pdf to check if "print" works - using PdfDocument pdfDocument = PdfReader.Open(memStm, PdfDocumentOpenMode.Import); - Assert.Equal(1+10, pdfDocument.PageCount); + var pdf = new PdfBinaryInspector(memStm); - Assert.True(true); + Assert.True(pdf.HasPdfHeader); + Assert.Equal(1 + 10, pdf.PageCount); } [Theory()] @@ -74,50 +75,5 @@ public void TestdrawinginvalidUdfsOutPut(string file) }); } - private IEnumerable ExtractLines(PdfPage p) - { - var h = p.Height.Point; - var content = ContentReader.ReadContent(p); - - foreach (COperator op in content) - { - string s = string.Empty; - switch (op.OpCode.OpCodeName) - { - case OpCodeName.m: - s = "MOVETO " + ExtractLineOperands(op.Operands, h); - yield return s; - break; - case OpCodeName.l: - s = "LINETO " + ExtractLineOperands(op.Operands, h); ; - yield return s; - break; - default: - break; - } - } - } - - string ExtractLineOperands(CSequence cSequence, double pageHeight) - { - List lines = new List(); - int i = 0; - foreach (var operand in cSequence) - { - if (operand is CInteger intValue) - { - if (i == 1) - { - lines.Add((pageHeight - intValue.Value).ToString()); - } - else - { - lines.Add(intValue.ToString()); - } - } - i++; - } - return string.Join(",", lines.ToArray()); - } } } \ No newline at end of file diff --git a/pdfsharpdslTests/Parser/PdfDrawerForTestsVisitor.cs b/pdfsharpdslTests/Parser/PdfDrawerForTestsVisitor.cs index e2a7a80..34d1309 100644 --- a/pdfsharpdslTests/Parser/PdfDrawerForTestsVisitor.cs +++ b/pdfsharpdslTests/Parser/PdfDrawerForTestsVisitor.cs @@ -7,7 +7,6 @@ using System.Text; using System.Threading.Tasks; using Irony.Parsing; -using PdfSharpCore.Drawing; namespace pdfsharpdslTests { diff --git a/pdfsharpdslTests/Parser/TextDocumentDrawer.cs b/pdfsharpdslTests/Parser/TextDocumentDrawer.cs index a2d28f6..20959a4 100644 --- a/pdfsharpdslTests/Parser/TextDocumentDrawer.cs +++ b/pdfsharpdslTests/Parser/TextDocumentDrawer.cs @@ -1,6 +1,4 @@ -using PdfSharpCore; -using PdfSharpCore.Drawing; -using PdfSharpDslCore.Drawing; +using PdfSharpDslCore.Drawing; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -16,10 +14,10 @@ namespace pdfsharpdslTests internal class TextDocumentDrawer : IPdfDocumentDrawer { public StringBuilder OutputRendering { get; private set; } = new StringBuilder(); - public XPen CurrentPen { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public XBrush CurrentBrush { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public XBrush? HighlightBrush { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public XFont CurrentFont + public PdfPen CurrentPen { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public PdfBrush CurrentBrush { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public PdfBrush? HighlightBrush { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public PdfFont CurrentFont { get => throw new NotImplementedException(); set @@ -38,7 +36,7 @@ public void DrawEllipse(double x, double y, double w, double h, bool isFilled) throw new NotImplementedException(); } - public void DrawImage(XImage image, double x, double y, double? w, double? h, bool sizeInPixel, bool cropImage) + public void DrawImage(PdfImage image, double x, double y, double? w, double? h, bool sizeInPixel, bool cropImage) { throw new NotImplementedException(); } @@ -53,7 +51,7 @@ public void DrawPie(double x, double y, double? w, double? h, double startAngle, throw new NotImplementedException(); } - public void DrawPolygon(IEnumerable points, bool isFilled) + public void DrawPolygon(IEnumerable points, bool isFilled) { throw new NotImplementedException(); } @@ -72,7 +70,7 @@ public void DrawText(string text, double x, double y, double? w, double? h) { throw new NotImplementedException(); } - public void DrawLineText(string text, double x, double y, double? w, double? h, XStringAlignment hAlign, XLineAlignment vAlign, TextOrientation textOrientation) + public void DrawLineText(string text, double x, double y, double? w, double? h, PdfHorizontalAlignment hAlign, PdfVerticalAlignment vAlign, TextOrientation textOrientation) { var halign = $"HAlign={ToHAlign(hAlign)}"; OutputRendering.Append($"LINETEXT "); @@ -89,35 +87,35 @@ public void DrawLineText(string text, double x, double y, double? w, double? h, } - public void DrawTitle(string text, double margin, XStringAlignment hAlign, XLineAlignment vAlign) + public void DrawTitle(string text, double margin, PdfHorizontalAlignment hAlign, PdfVerticalAlignment vAlign) { var halign = $"HAlign={ToHAlign(hAlign)}"; OutputRendering.AppendLine($"TITLE Margin={margin.ToString(CultureInfo.InvariantCulture)} {halign} Text=\"{text}\";"); } - private string ToHAlign(XStringAlignment hAlign) + private string ToHAlign(PdfHorizontalAlignment hAlign) { switch (hAlign) { - case XStringAlignment.Near: + case PdfHorizontalAlignment.Near: return "left"; - case XStringAlignment.Center: + case PdfHorizontalAlignment.Center: return "hcenter"; - case XStringAlignment.Far: + case PdfHorizontalAlignment.Far: return "right"; default: return "left"; } } - private string ToVAlign(XLineAlignment vAlign) + private string ToVAlign(PdfVerticalAlignment vAlign) { switch (vAlign) { - case XLineAlignment.Near: + case PdfVerticalAlignment.Near: return "top"; - case XLineAlignment.Center: + case PdfVerticalAlignment.Center: return "vcenter"; - case XLineAlignment.Far: + case PdfVerticalAlignment.Far: return "bottom"; default: return "top"; @@ -134,7 +132,7 @@ public void MoveTo(double x, double y) throw new NotImplementedException(); } - public void NewPage(PageSize? pageSize = null, PageOrientation? pageOrientation = null) + public void NewPage(PdfPageSize? pageSize = null, PdfPageOrientation? pageOrientation = null) { throw new NotImplementedException(); } diff --git a/pdfsharpdslTests/ParserTests.cs b/pdfsharpdslTests/ParserTests.cs index 3ccb9bc..25b3150 100644 --- a/pdfsharpdslTests/ParserTests.cs +++ b/pdfsharpdslTests/ParserTests.cs @@ -21,6 +21,14 @@ public void CheckGrammarErrors() Assert.Empty(p.Language.Errors); } + [Theory] + [InlineData("SET FONT Name=\"Arial\" Size=11 bolditalic;")] + [InlineData("SET BRUSH lightsalmon;")] + public void ParsesSupportedStyleAndColorKeywords(string input) + { + ParseText(input); + } + [Theory()] [InlineData("pdf1.txt")] diff --git a/pdfsharpdslTests/PdfBinaryInspector.cs b/pdfsharpdslTests/PdfBinaryInspector.cs new file mode 100644 index 0000000..7c883ea --- /dev/null +++ b/pdfsharpdslTests/PdfBinaryInspector.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +namespace pdfsharpdslTests; + +internal sealed class PdfBinaryInspector +{ + private static readonly Regex PagePattern = new(@"/Type\s*/Page(?!s)\b", RegexOptions.Compiled); + private static readonly Regex MediaBoxPattern = new( + @"/MediaBox\s*\[\s*(?-?[\d.]+)\s+(?-?[\d.]+)\s+(?-?[\d.]+)\s+(?-?[\d.]+)\s*\]", + RegexOptions.Compiled); + + private readonly byte[] _bytes; + private readonly string _text; + + public PdfBinaryInspector(Stream stream) + { + stream.Position = 0; + using var copy = new MemoryStream(); + stream.CopyTo(copy); + _bytes = copy.ToArray(); + _text = Encoding.Latin1.GetString(_bytes); + } + + public bool HasPdfHeader => _bytes.Length >= 5 && Encoding.ASCII.GetString(_bytes, 0, 5) == "%PDF-"; + public int PageCount => PagePattern.Matches(_text).Count; + public int ContentStreamCount => Regex.Matches(_text, @"\bstream\r?\n").Count; + public int FontCount => Regex.Matches(_text, @"/Type\s*/Font\b").Count; + public int ImageCount => Regex.Matches(_text, @"/Subtype\s*/Image\b").Count; + + public IReadOnlyList MediaBoxes => MediaBoxPattern.Matches(_text) + .Select(match => new PdfRectInfo( + Parse(match.Groups["x1"].Value), + Parse(match.Groups["y1"].Value), + Parse(match.Groups["x2"].Value), + Parse(match.Groups["y2"].Value))) + .ToArray(); + + private static double Parse(string value) => double.Parse(value, CultureInfo.InvariantCulture); +} + +internal readonly record struct PdfRectInfo(double X1, double Y1, double X2, double Y2) +{ + public double Width => X2 - X1; + public double Height => Y2 - Y1; +} \ No newline at end of file diff --git a/pdfsharpdslTests/PdfDocumentDrawerTests.cs b/pdfsharpdslTests/PdfDocumentDrawerTests.cs index 685f1ac..4a5e8f2 100644 --- a/pdfsharpdslTests/PdfDocumentDrawerTests.cs +++ b/pdfsharpdslTests/PdfDocumentDrawerTests.cs @@ -1,36 +1,163 @@ -using PdfSharpCore; -using PdfSharpCore.Pdf; using PdfSharpDslCore.Drawing; namespace pdfsharpdslTests { public class PdfDocumentDrawerTests { + [Fact] + public void DslOrientationsProduceRotatedTextMatrices() + { + var parser = new Irony.Parsing.Parser(new PdfSharpDslCore.Parser.PdfGrammar()); + var tree = parser.Parse( + "LINETEXT 220,235 HAlign=left VAlign=vcenter Orientation=vertical Text=\"vertical\";" + + "LINETEXT 300,235 HAlign=left VAlign=vcenter Orientation=30 Text=\"30 degree rotation\";"); + Assert.False(tree.HasErrors()); + using var drawer = new PdfDocumentDrawer(); + new PdfSharpDslCore.Parser.PdfDrawerVisitor().Draw(drawer, tree); + var content = ReadContent(drawer.PublishPdf()); + + Assert.Matches(@"0\.000000 -1\.000000 1\.000000 0\.000000 220\.00 [\d.]+ Tm\n\(vertical\) Tj", content); + Assert.Matches(@"0\.866025 -0\.500000 0\.500000 0\.866025 300\.00 [\d.]+ Tm\n\(30 degree rotation\) Tj", content); + } + + [Fact] + public void PolygonsKeepStylesFromTheirDrawingInstructions() + { + var parser = new Irony.Parsing.Parser(new PdfSharpDslCore.Parser.PdfGrammar()); + var tree = parser.Parse( + "SET PEN darkslategray 2 solid;POLYGON 10,20,30,20,20,40;" + + "SET BRUSH lightseagreen;FILLPOLYGON 50,20,70,20,60,40;" + + "SET PEN crimson 0.5 solid;SET BRUSH black;"); + Assert.False(tree.HasErrors()); + using var drawer = new PdfDocumentDrawer(); + new PdfSharpDslCore.Parser.PdfDrawerVisitor().Draw(drawer, tree); + + var content = ReadContent(drawer.PublishPdf()); + + Assert.Contains("2.00 w\n0.1843 0.3098 0.3098 RG\n", content); + Assert.Contains("0.1255 0.6980 0.6667 rg\n", content); + Assert.Contains("h\nS\n", content); + Assert.Contains("h\nB\n", content); + Assert.DoesNotContain("0.0000 0.0000 0.0000 rg\n", content); + } + + [Fact] + public void LineToKeepsEachStartPointAndPenAtRecordingTime() + { + using var drawer = new PdfDocumentDrawer(); + drawer.CurrentPen = new PdfPen(new PdfColor(255, 128, 0, 128), 3); + drawer.MoveTo(10, 20); + drawer.LineTo(30, 40); + drawer.LineTo(50, 20); + drawer.CurrentPen = new PdfPen(PdfColor.Black, 1); + drawer.MoveTo(100, 100); + + var content = ReadContent(drawer.PublishPdf()); + + Assert.Contains("0.5020 0.0000 0.5020 RG\n", content); + Assert.Contains("3.00 w\n", content); + Assert.Contains("10.00 821.89 m\n30.00 801.89 l\n", content); + Assert.Contains("30.00 801.89 m\n50.00 821.89 l\n", content); + } + + [Fact] + public void DashStylesUseNativePdfPatternAndDoNotLeak() + { + var parser = new Irony.Parsing.Parser(new PdfSharpDslCore.Parser.PdfGrammar()); + var tree = parser.Parse( + "SET PEN black 2 dash;LINE 10,20,110,20;" + + "SET PEN black 2 solid;LINE 10,40,110,40;"); + Assert.False(tree.HasErrors()); + using var drawer = new PdfDocumentDrawer(); + new PdfSharpDslCore.Parser.PdfDrawerVisitor().Draw(drawer, tree); + + var content = ReadContent(drawer.PublishPdf()); + + Assert.Contains("[8.00 6.00] 0.00 d\n", content); + Assert.Contains("q\n[8.00 6.00] 0.00 d\n", content); + Assert.Contains("Q\n2.00 w\n0.0000 0.0000 0.0000 RG\n10.00 801.89 m\n110.00 801.89 l\nS\n", content); + } + + [Fact] + public void DashStylesApplyToRectangleOutlines() + { + var parser = new Irony.Parsing.Parser(new PdfSharpDslCore.Parser.PdfGrammar()); + var tree = parser.Parse("SET PEN crimson 1 dash;RECT 20,40,-20,-55;"); + Assert.False(tree.HasErrors()); + using var drawer = new PdfDocumentDrawer(); + new PdfSharpDslCore.Parser.PdfDrawerVisitor().Draw(drawer, tree); + + var content = ReadContent(drawer.PublishPdf()); + + Assert.Contains("q\n[4.00 3.00] 0.00 d\n", content); + Assert.Contains("re\nS\nQ\n", content); + } + + [Fact] + public void TablesMeasureAutoColumnsAndMultilineRows() + { + using var drawer = new PdfDocumentDrawer(); + var table = new TableDefinition(); + table.Columns.Add(new ColumnDefinition { ColumnHeaderName = "Feature", MaxWidth = 130, Font = new PdfFont("Arial", 9) }); + table.Columns.Add(new ColumnDefinition { ColumnHeaderName = "Syntax", DesiredWidth = 140, MaxWidth = 180, Font = new PdfFont("Arial", 9) }); + table.Columns.Add(new ColumnDefinition { ColumnHeaderName = "Notes", MaxWidth = 240, Font = new PdfFont("Arial", 9) }); + table.Rows.Add(new RowDefinition { Data = ["Automatic sizing", "Width=auto", "Desired width follows content up to MaxWidth"] }); + table.Rows.Add(new RowDefinition { Data = ["Multiline cell", "COL expression", "Line one\r\nLine two\r\nLine three"] }); + + drawer.DrawTable(40, 90, table); + + Assert.True(table.Columns[0].DesiredWidth > 0); + Assert.Equal(140, table.Columns[1].DrawWidth); + Assert.True(table.Columns[2].DesiredWidth > table.Columns[2].ColumnHeaderName.Length); + Assert.True(table.Rows[1].DesiredHeight > table.Rows[0].DesiredHeight); + } + + private static string ReadContent(byte[] pdf) + { + var raw = System.Text.Encoding.Latin1.GetString(pdf); + var content = new System.Text.StringBuilder(); + foreach (System.Text.RegularExpressions.Match match in System.Text.RegularExpressions.Regex.Matches( + raw, @"stream\r?\n(?.*?)\r?\nendstream", System.Text.RegularExpressions.RegexOptions.Singleline)) + { + using var compressed = new MemoryStream(System.Text.Encoding.Latin1.GetBytes(match.Groups["data"].Value)); + using var inflated = new System.IO.Compression.ZLibStream(compressed, System.IO.Compression.CompressionMode.Decompress); + using var reader = new StreamReader(inflated, System.Text.Encoding.Latin1); + content.Append(reader.ReadToEnd()); + } + + return content.ToString(); + } + [Fact] public void NewPageUsesDefaultsAndPersistsExplicitSettings() { - using var document = new PdfDocument(); - using var drawer = new PdfDocumentDrawer(document); + using var drawer = new PdfDocumentDrawer(); var pageNumbers = new List(); Action callback = pageNumbers.Add; drawer.RegisterOnNewPage(callback); drawer.RegisterOnNewPage(callback); - Assert.Equal(PageSize.A4, drawer.CurrentPage.Size); - Assert.Equal(PageOrientation.Portrait, drawer.CurrentPage.Orientation); + Assert.Equal(595.28, drawer.PageWidth, 2); + Assert.Equal(841.89, drawer.PageHeight, 2); - drawer.NewPage(PageSize.Letter, PageOrientation.Landscape); - Assert.Equal(PageSize.Letter, drawer.CurrentPage.Size); - Assert.Equal(PageOrientation.Landscape, drawer.CurrentPage.Orientation); + drawer.NewPage(PdfPageSize.Letter, PdfPageOrientation.Landscape); + Assert.Equal(792, drawer.PageWidth); + Assert.Equal(612, drawer.PageHeight); drawer.NewPage(); - Assert.Equal(PageSize.Letter, drawer.CurrentPage.Size); - Assert.Equal(PageOrientation.Landscape, drawer.CurrentPage.Orientation); + Assert.Equal(792, drawer.PageWidth); + Assert.Equal(612, drawer.PageHeight); Assert.Equal(new[] { 2, 3 }, pageNumbers); drawer.UnRegisterOnNewPage(callback); drawer.NewPage(); Assert.Equal(new[] { 2, 3 }, pageNumbers); + + using var stream = new MemoryStream(); + drawer.PublishPdf(stream); + var pdf = new PdfBinaryInspector(stream); + Assert.Equal(4, pdf.PageCount); + Assert.Equal(4, pdf.MediaBoxes.Count); } } } \ No newline at end of file diff --git a/pdfsharpdslTests/RenderingTests.cs b/pdfsharpdslTests/RenderingTests.cs index dfe9d56..084336e 100644 --- a/pdfsharpdslTests/RenderingTests.cs +++ b/pdfsharpdslTests/RenderingTests.cs @@ -1,5 +1,4 @@ -using PdfSharpCore.Pdf; -using PdfSharpDslCore.Drawing; +using PdfSharpDslCore.Drawing; using PdfSharpDslCore.Parser; using System; using System.Collections.Generic; diff --git a/pdfsharpdslTests/ReplayerTests/InstructionRecorderTests.cs b/pdfsharpdslTests/ReplayerTests/InstructionRecorderTests.cs index 2e55e26..67483f9 100644 --- a/pdfsharpdslTests/ReplayerTests/InstructionRecorderTests.cs +++ b/pdfsharpdslTests/ReplayerTests/InstructionRecorderTests.cs @@ -1,6 +1,4 @@ using Moq; -using PdfSharpCore; -using PdfSharpCore.Drawing; using PdfSharpDslCore.Drawing; using System; using System.Collections.Generic; @@ -20,15 +18,15 @@ public class InstructionRecorderTests #region private classes class DummyInstruction : IInstruction { - public DummyInstruction(XRect r, string name="") + public DummyInstruction(PdfRect r, string name="") { this.Rect = r; Name = name; } - public XRect Rect { get; } + public PdfRect Rect { get; } public string Name { get; } - public XRect DrawingRect { get; private set; } + public PdfRect DrawingRect { get; private set; } public double Draw(IPdfDocumentDrawer drawer, double offsetY, double pageOffsetY) { @@ -58,8 +56,8 @@ public void RecorderTests_Level1() var block = recorder.OpenBlock(string.Empty,0, true,0); Assert.NotNull(block); - var r = new XRect(0, 0, 50, 50); - var r1 = new XRect(150, 150, 50, 50); + var r = new PdfRect(0, 0, 50, 50); + var r1 = new PdfRect(150, 150, 50, 50); block.PushInstruction(new DummyInstruction(r)); @@ -71,7 +69,7 @@ public void RecorderTests_Level1() var hasNewPage = block.Draw(drawerMock.Object, 0,0); Assert.Equal(0,hasNewPage); - block.PushInstruction(new DummyInstruction(new XRect(0, 200, 10, 100))); + block.PushInstruction(new DummyInstruction(new PdfRect(0, 200, 10, 100))); hasNewPage = block.Draw(drawerMock.Object, 0, 0); Assert.True(hasNewPage > 0); drawerMock.Verify(x => x.NewPage(null, null), Times.Once); @@ -79,11 +77,11 @@ public void RecorderTests_Level1() //draw at bottom page block = recorder.OpenBlock(string.Empty,200, true,0); - var instr = new DummyInstruction(new XRect(0, 0, 50, 100)); + var instr = new DummyInstruction(new PdfRect(0, 0, 50, 100)); block.PushInstruction(instr); hasNewPage = block.Draw(drawerMock.Object, 0, 0); Assert.True(hasNewPage > 0); - Assert.Equal(new XRect(0,0,50,100), instr.DrawingRect); + Assert.Equal(new PdfRect(0,0,50,100), instr.DrawingRect); recorder.CloseBlock(); @@ -97,7 +95,7 @@ public void DrawAtBottomPage() var recorder = new BlocksRecorder(); //draw at bottom page var block = recorder.OpenBlock(string.Empty,200, true, 0); - var instr = new DummyInstruction(new XRect(0, 0, 50, 100)); + var instr = new DummyInstruction(new PdfRect(0, 0, 50, 100)); block.PushInstruction(instr); var hasNewPage = block.Draw(drawerMock.Object, 0, 0); @@ -138,20 +136,20 @@ public void RecorderTests_block_with_offsetY() var block = recorder.OpenBlock(string.Empty,200, true,0); AddInstructions(block, 1, 100); - Assert.Equal(new XRect(0,200, 50, 100), block.Rect); + Assert.Equal(new PdfRect(0,200, 50, 100), block.Rect); recorder.CloseBlock(); block = recorder.OpenBlock(string.Empty,100, true); var block1 = recorder.OpenBlock(string.Empty,100, true); AddInstructions(block1, 1, 100); - Assert.Equal(new XRect(0,200, 50, 100), block.Rect); - Assert.Equal(new XRect(0,100, 50, 100), block1.Rect); + Assert.Equal(new PdfRect(0,200, 50, 100), block.Rect); + Assert.Equal(new PdfRect(0,100, 50, 100), block1.Rect); } [Fact] public void InstructionActionExecutesWithOffsetAndExposesMetadata() { - var rectangle = new XRect(1, 2, 3, 4); + var rectangle = new PdfRect(1, 2, 3, 4); double? appliedOffset = null; var instruction = new InstructionAction(offset => appliedOffset = offset, rectangle, "action"); @@ -167,7 +165,7 @@ public void InstructionActionExecutesWithOffsetAndExposesMetadata() public void RecorderRootRejectsInstructionsAndBlockMetadataCanBeCleared() { var recorder = new BlocksRecorder(); - var instruction = new DummyInstruction(new XRect(0, 0, 10, 10)); + var instruction = new DummyInstruction(new PdfRect(0, 0, 10, 10)); Assert.False(recorder.CanPushInstruction); Assert.Throws(() => recorder.CurrentBlock.PushInstruction(instruction)); @@ -195,13 +193,13 @@ public void NestedBlockMovesToNextPage() var recorder = new BlocksRecorder(logger.Object); var outer = recorder.OpenBlock("outer", 0, false); var child = outer.OpenBlock("child", 250, true); - var instruction = new DummyInstruction(new XRect(0, 0, 50, 50)); + var instruction = new DummyInstruction(new PdfRect(0, 0, 50, 50)); child.PushInstruction(instruction); var pageOffset = outer.Draw(drawer.Object, 0, 0); drawer.Verify(x => x.NewPage(null, null), Times.Once); - Assert.Equal(new XRect(0, 0, 50, 50), instruction.DrawingRect); + Assert.Equal(new PdfRect(0, 0, 50, 50), instruction.DrawingRect); Assert.True(pageOffset > 0); } @@ -212,7 +210,7 @@ public void OversizedNestedBlockCannotBePrintedEntirely() var recorder = new BlocksRecorder(); var outer = recorder.OpenBlock("outer", 0, false); var child = outer.OpenBlock("child", 0, true); - child.PushInstruction(new DummyInstruction(new XRect(0, 0, 50, 400))); + child.PushInstruction(new DummyInstruction(new PdfRect(0, 0, 50, 400))); Assert.Throws(() => outer.Draw(drawer.Object, 0, 0)); } @@ -223,10 +221,10 @@ public void InstructionsCannotBeAddedWhileBlockIsDrawing() var drawer = defaultDrawerMock(); var recorder = new BlocksRecorder(); var block = recorder.OpenBlock("block", 0, true); - var extraInstruction = new DummyInstruction(new XRect(0, 20, 10, 10)); + var extraInstruction = new DummyInstruction(new PdfRect(0, 20, 10, 10)); block.PushInstruction(new InstructionAction( _ => block.PushInstruction(extraInstruction), - new XRect(0, 0, 10, 10), + new PdfRect(0, 0, 10, 10), "mutating")); block.Draw(drawer.Object, 0, 0); @@ -236,7 +234,7 @@ public void InstructionsCannotBeAddedWhileBlockIsDrawing() private static void AddInstructions(IInstructionBlock block, int count, int height) { - var r = new XRect(0, 0, 50, height); + var r = new PdfRect(0, 0, 50, height); for (var i = 0; i < count; i++) { diff --git a/pdfsharpdslTests/SourceGenerator/SourceGeneratorTests.cs b/pdfsharpdslTests/SourceGenerator/SourceGeneratorTests.cs index a64dc11..c7204ae 100644 --- a/pdfsharpdslTests/SourceGenerator/SourceGeneratorTests.cs +++ b/pdfsharpdslTests/SourceGenerator/SourceGeneratorTests.cs @@ -2,7 +2,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; -using PdfSharpCore.Drawing; using PdfSharpDslCore.Drawing; using PdfSharpDslCore.Generator; using System.Diagnostics.CodeAnalysis; @@ -31,7 +30,7 @@ public void GeneratorProducesCompilableSourceFromTaggedAdditionalFile() var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) .Split(Path.PathSeparator) .Append(typeof(IPdfDocumentDrawer).Assembly.Location) - .Append(typeof(XColor).Assembly.Location) + .Append(typeof(PdfColor).Assembly.Location) .Distinct() .Select(path => MetadataReference.CreateFromFile(path)); var compilation = CSharpCompilation.Create( diff --git a/pdfsharpdslTests/ValidInputFiles/pdf1-table-merged.txt b/pdfsharpdslTests/ValidInputFiles/pdf1-table-merged.txt deleted file mode 100644 index 3ca9744..0000000 --- a/pdfsharpdslTests/ValidInputFiles/pdf1-table-merged.txt +++ /dev/null @@ -1,35 +0,0 @@ -# merged cells, per-cell alignment and multiline content - -SET FONT Name="Arial" Size=9 regular; -SET PEN darkslategray 1 solid; - -TABLE 45,90 -HEAD 0xffc8d9ed -COL Width=125 MaxWidth=125 FONT="Arial",9,bold darkblue 0xffe8f1fb "Team"; -COL Width=125 MaxWidth=125 FONT="Arial",9,bold darkgreen 0xffe8f7ee "Q1"; -COL Width=125 MaxWidth=125 FONT="Arial",9,bold darkslategray 0xfffff3d6 "Q2"; -COL Width=125 MaxWidth=125 FONT="Arial",9,bold maroon 0xffffe4e1 "Status"; -ENDHEAD -ROW 54 -COL RowSpan=2 HAlign=hcenter VAlign=vcenter "Platform"; -COL ColSpan=2 HAlign=hcenter VAlign=vcenter "First-half delivery"; -COL RowSpan=2 HAlign=hcenter VAlign=vcenter "On track"; -ENDROW -ROW 34 -COL HAlign=right VAlign=vcenter "Q1: 128"; -COL HAlign=right VAlign=vcenter "Q2: 146"; -ENDROW -ROW 48 -COL ColSpan=4 HAlign=hcenter VAlign=vcenter "One cell spans the complete table width"; -ENDROW -ROW 64 -COL HAlign=left VAlign=top "left / top"; -COL HAlign=hcenter VAlign=vcenter "center / center"; -COL HAlign=right VAlign=bottom "right / bottom"; -COL HAlign=hcenter VAlign=vcenter "Multiline\r\ncentered"; -ENDROW -ROW 44 -COL ColSpan=2 HAlign=left VAlign=vcenter "Two-column summary"; -COL ColSpan=2 HAlign=right VAlign=vcenter "Formula span: "+(1+1); -ENDROW -ENDTABLE diff --git a/pdfsharpdslTests/VisitorTests.cs b/pdfsharpdslTests/VisitorTests.cs index bce8941..f61d7d0 100644 --- a/pdfsharpdslTests/VisitorTests.cs +++ b/pdfsharpdslTests/VisitorTests.cs @@ -1,7 +1,6 @@ using Castle.Components.DictionaryAdapter.Xml; using Irony.Parsing; using Moq; -using PdfSharpCore.Drawing; using PdfSharpDslCore.Drawing; using PdfSharpDslCore.Parser; using System; @@ -25,7 +24,7 @@ public void TestValidFiles() mock.SetupProperty(x => x.CurrentBrush); new PdfDrawerVisitor().Draw(mock.Object, res); - Assert.Equal(XColors.Black, ((XSolidBrush)mock.Object.CurrentBrush).Color); + Assert.Equal(PdfColor.Black, mock.Object.CurrentBrush.Color); } [Theory] @@ -54,16 +53,14 @@ public void TestFormulaEvaluatorWithFontName(string input, object expected, int { var res = ParseText(input); var mock = new Mock(); - mock.SetupProperty(x => x.CurrentFont, new XFont("Consolas", 8)); + mock.SetupProperty(x => x.CurrentFont, new PdfFont("Consolas", 8)); var visitor = new PdfDrawerForTestsVisitor(); visitor.RegisterFormulaFunction("getFontName", (_) => expected); var drawer = mock.Object; visitor.Draw(drawer, res); var f = drawer.CurrentFont; - //because font names change on different OS - var pi = typeof(XFont).GetProperty("FamilyName", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - Assert.StartsWith((string)expected, (string)pi?.GetValue(drawer.CurrentFont)!); + Assert.StartsWith((string)expected, drawer.CurrentFont.FamilyName); Assert.Equal(size, drawer.CurrentFont.Size); } @@ -119,6 +116,25 @@ public void DrawExecutesViewSizeTextWidthAndDebugOptions() drawer.Object.DebugOptions); } + [Theory] + [InlineData("", TextOrientationEnum.Horizontal, null)] + [InlineData("Orientation=horizontal", TextOrientationEnum.Horizontal, null)] + [InlineData("Orientation=vertical", TextOrientationEnum.Vertical, null)] + [InlineData("Orientation=30", TextOrientationEnum.Horizontal, 30.0)] + [InlineData("Orientation=(-15*2)", TextOrientationEnum.Horizontal, -30.0)] + public void LineTextPreservesOrientation(string orientation, TextOrientationEnum expectedMode, double? expectedAngle) + { + var tree = ParseText($"LINETEXT 220,235 HAlign=left VAlign=vcenter {orientation} Text=\"label\";"); + Assert.False(tree.HasErrors()); + var drawer = new Mock(); + + new PdfDrawerVisitor().Draw(drawer.Object, tree); + + drawer.Verify(target => target.DrawLineText("label", 220, 235, null, null, + PdfHorizontalAlignment.Near, PdfVerticalAlignment.Center, + It.Is(value => value.Orientation == expectedMode && value.Angle == expectedAngle)), Times.Once); + } + [Fact] public void DrawResolvesPageSystemVariables() { @@ -145,14 +161,14 @@ public void ImageStatementsPreserveDimensionsUnitsAndCropMode() var calls = new List<(double X, double Y, double? Width, double? Height, bool Pixel, bool Crop)>(); var drawer = new Mock(); drawer.Setup(x => x.DrawImage( - It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((_, x, y, width, height, pixel, crop) => + .Callback((_, x, y, width, height, pixel, crop) => calls.Add((x, y, width, height, pixel, crop))); new PdfDrawerVisitor().Draw(drawer.Object, tree); @@ -195,47 +211,34 @@ public void TableRowTemplateBuildsRowsWidthsAndPadding() } [Fact] - public void TableColParsesColSpanRowSpanAndAlignment() + public void TableCellsCaptureSpansAndAlignment() { var tree = ParseText( "TABLE 20,30 " + "HEAD " + - "COL Width=40 MaxWidth=30 \"A\"; " + - "COL Width=auto MaxWidth=100 \"B\"; " + + "COL Width=100 MaxWidth=100 \"A\"; " + + "COL Width=100 MaxWidth=100 \"B\"; " + + "COL Width=100 MaxWidth=100 \"C\"; " + "ENDHEAD " + - "ROW " + - "COL ColSpan=2 HAlign=hcenter VAlign=vcenter \"merged\"; " + - "ENDROW " + - "ROW " + - "COL RowSpan=2 \"plain\"; " + - "COL \"formula: \"+(1+1); " + + "ROW 40 " + + "COL ColSpan=2 RowSpan=2 HAlign=hcenter VAlign=bottom \"Merged\"; " + + "COL HAlign=right \"Right\"; " + "ENDROW " + + "ROW COL \"Remaining\"; ENDROW " + "ENDTABLE"); TableDefinition? capturedTable = null; var drawer = new Mock(); drawer.Setup(x => x.DrawTable(20, 30, It.IsAny())) .Callback((_, _, table) => capturedTable = table); - var visitor = new InspectablePdfDrawerVisitor(); - visitor.Draw(drawer.Object, tree); + new InspectablePdfDrawerVisitor().Draw(drawer.Object, tree); - Assert.NotNull(capturedTable); - var mergedCell = capturedTable.Rows[0].Cells[0]; - Assert.Equal("merged", mergedCell.Text); + var mergedCell = Assert.Single(capturedTable!.Rows[0].Cells, cell => cell.Text == "Merged"); Assert.Equal(2, mergedCell.ColumnSpan); - Assert.Equal(1, mergedCell.RowSpan); - Assert.Equal(XStringAlignment.Center, mergedCell.HorizontalAlignment); - Assert.Equal(XLineAlignment.Center, mergedCell.VerticalAlignment); - - var plainCell = capturedTable.Rows[1].Cells[0]; - Assert.Equal("plain", plainCell.Text); - Assert.Equal(1, plainCell.ColumnSpan); - Assert.Equal(2, plainCell.RowSpan); - Assert.Null(plainCell.HorizontalAlignment); - Assert.Null(plainCell.VerticalAlignment); - - var formulaCell = capturedTable.Rows[1].Cells[1]; - Assert.Equal("formula: 2", formulaCell.Text); + Assert.Equal(2, mergedCell.RowSpan); + Assert.Equal(PdfHorizontalAlignment.Center, mergedCell.HorizontalAlignment); + Assert.Equal(PdfVerticalAlignment.Far, mergedCell.VerticalAlignment); + Assert.Equal(PdfHorizontalAlignment.Far, capturedTable.Rows[0].Cells[1].HorizontalAlignment); } [Fact] @@ -339,8 +342,8 @@ public void RowTemplateTracksOffsetsAndFinalHeight() var tree = ParseText("ROWTEMPLATE Count=2 Y=10 Name=\"row\" BorderSize=2 NewPageTopMargin=5 LINE 0,0,10,10; ENDROWTEMPLATE"); var drawer = new Mock(); drawer.SetupSequence(x => x.EndDrawRowTemplate(It.IsAny())) - .Returns(new DrawingResult { DrawingRect = new XRect(0, 0, 10, 20), PageOffsetY = 0 }) - .Returns(new DrawingResult { DrawingRect = new XRect(0, 20, 10, 30), PageOffsetY = 10 }); + .Returns(new DrawingResult { DrawingRect = new PdfRect(0, 0, 10, 20), PageOffsetY = 0 }) + .Returns(new DrawingResult { DrawingRect = new PdfRect(0, 20, 10, 30), PageOffsetY = 10 }); var visitor = new InspectablePdfDrawerVisitor(); visitor.Draw(drawer.Object, tree); diff --git a/pdfsharpdslTests/pdfsharpdslTests.csproj b/pdfsharpdslTests/pdfsharpdslTests.csproj index ba69c1c..6e511b9 100644 --- a/pdfsharpdslTests/pdfsharpdslTests.csproj +++ b/pdfsharpdslTests/pdfsharpdslTests.csproj @@ -1,24 +1,23 @@ - net10.0 + $(NetAppTfm) enable - enable false - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -60,9 +59,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest diff --git a/tasks.md b/tasks.md deleted file mode 100644 index 23f1dc6..0000000 --- a/tasks.md +++ /dev/null @@ -1,218 +0,0 @@ -# .NET 10 Upgrade Tasks - -Coverage work blocks the framework upgrade. `PdfSharpDslCore` line coverage must be at least 90%; branch coverage is reported but does not gate progress. - -**Current verified coverage (2026-09-18):** 90.76% lines (2260/2490), 83.56% branches (778/931), 83 tests passed. - -## TASK-001: Coverage baseline - -**Status:** Complete (2026-09-18) -**Depends on:** None - -**Objective:** Establish reproducible, core-only Cobertura reporting and record the current baseline. - -**Affected files:** `.runsettings`, `scripts/coverage.ps1`, `tasks.md` - -**Acceptance criteria:** - -- The test suite runs through Coverlet's collector. -- The report contains only the `PdfSharpDslCore` assembly. -- The script prints line and branch totals and fails when the report is missing or contains another assembly. -- Generated reports remain under the ignored `artifacts/` directory. - -**Validation:** - -```powershell -powershell -NoProfile -File .\scripts\coverage.ps1 -``` - -**Result:** 45 tests passed. Line coverage is 79.63% (1983/2490); branch coverage is 73.14% (681/931). The migration remains blocked. - -**Handoff:** Begin TASK-002. Raise core line coverage above the current baseline without changing target frameworks or enforcing the final threshold yet. - -## TASK-002: Evaluation coverage - -**Status:** Not started -**Depends on:** TASK-001 - -**Objective:** Test arithmetic, comparison, boolean, unary, null, string, and error paths; investigate the `%` parser/evaluator mismatch. - -**Affected files:** `pdfsharpdslTests/FormulaTests.cs`, `PdfSharpDslCore/Evaluation/`, parser tests and parser implementation if the mismatch is confirmed - -**Acceptance criteria:** Evaluation branches have focused tests, the `%` behavior is documented by a test and corrected if defective, and all existing tests pass. - -**Validation:** - -```powershell -dotnet test .\pdfsharpdslTests\pdfsharpdslTests.csproj --filter FullyQualifiedName~FormulaTests -powershell -NoProfile -File .\scripts\coverage.ps1 -``` - -**Handoff:** Record the new coverage result here and identify the largest remaining uncovered core areas for TASK-003. - -## TASK-003: Variables and UDFs - -**Status:** Not started -**Depends on:** TASK-002 - -**Objective:** Cover variable lifecycle, nested scopes, system variables, UDF dispatch, failure paths, and `__ONNEWPAGE`. - -**Affected files:** `pdfsharpdslTests/`, `PdfSharpDslCore/` variable and UDF implementation - -**Acceptance criteria:** Focused tests cover successful and failing variable/UDF behavior, including nested scope and new-page callbacks. - -**Validation:** - -```powershell -dotnet test .\pdfsharpdslTests\pdfsharpdslTests.csproj -powershell -NoProfile -File .\scripts\coverage.ps1 -``` - -**Handoff:** Record coverage and list remaining visitor or drawing gaps for TASK-004. - -## TASK-004: Visitor and drawing helpers - -**Status:** Not started -**Depends on:** TASK-003 - -**Objective:** Cover visitor dispatch, page defaults, debug options, tables, images, alignment, clipping, and width calculations. - -**Affected files:** `pdfsharpdslTests/VisitorTests.cs`, `pdfsharpdslTests/RenderingTests.cs`, `PdfSharpDslCore/Drawing/`, visitor implementation - -**Acceptance criteria:** Deterministic tests cover the listed helper paths without requiring committed generated PDFs. - -**Validation:** - -```powershell -dotnet test .\pdfsharpdslTests\pdfsharpdslTests.csproj -powershell -NoProfile -File .\scripts\coverage.ps1 -``` - -**Handoff:** Record coverage and enumerate only the gaps needed to exceed 80% in TASK-005. - -## TASK-005: Enforce coverage - -**Status:** Not started -**Depends on:** TASK-004 - -**Objective:** Cover remaining pagination and PDF drawing paths, then enforce core line coverage of at least 90%. - -**Affected files:** Remaining core tests, `scripts/coverage.ps1`, CI workflow if present - -**Acceptance criteria:** `PdfSharpDslCore` line coverage is at least 90%, the script fails below 90%, and branch coverage remains informational. - -**Validation:** - -```powershell -powershell -NoProfile -File .\scripts\coverage.ps1 -``` - -**Handoff:** Unblock TASK-006 only after recording a passing line rate here. - -## TASK-006: Generator safety - -**Status:** Blocked by coverage -**Depends on:** TASK-005 - -**Objective:** Remove the hard-coded Debug assembly path and add a real Roslyn generator compilation test. - -**Affected files:** `PdfSharpDslCore.Generator/`, `pdfsharpdslTests/SourceGenerator/` - -**Acceptance criteria:** Generator tests compile a representative input and Debug and Release builds succeed without configuration-specific paths. - -**Validation:** - -```powershell -dotnet test .\pdfsharpdslTests\pdfsharpdslTests.csproj --filter FullyQualifiedName~SourceGenerator -dotnet build .\bnf_and_pdf.sln --configuration Debug -dotnet build .\bnf_and_pdf.sln --configuration Release -``` - -**Handoff:** Document generator loading assumptions before dependency upgrades. - -## TASK-007: Dependencies - -**Status:** Blocked by coverage -**Depends on:** TASK-006 - -**Objective:** Upgrade test, Coverlet, Roslyn, logging, image, font, and archive packages; resolve ImageSharp advisories. - -**Affected files:** Project files and package lock/restore outputs where applicable - -**Acceptance criteria:** Restored packages have no known ImageSharp advisories, core and generator retain `netstandard2.0`, and tests and coverage remain passing. - -**Validation:** - -```powershell -dotnet restore .\bnf_and_pdf.sln -dotnet list .\bnf_and_pdf.sln package --vulnerable --include-transitive -dotnet test .\pdfsharpdslTests\pdfsharpdslTests.csproj -powershell -NoProfile -File .\scripts\coverage.ps1 -``` - -**Handoff:** Record selected package versions and any .NET 10 compatibility constraints. - -## TASK-008: Framework migration - -**Status:** Blocked by coverage -**Depends on:** TASK-007 - -**Objective:** Pin .NET 10, move console and tests to `net10.0`, retain reusable projects on `netstandard2.0`, and update GitHub Actions. - -**Affected files:** `global.json`, console/test project files, `.github/workflows/` - -**Acceptance criteria:** The intended SDK is selected, console and tests target `net10.0`, core and generator target `netstandard2.0`, and workflows use `actions/setup-dotnet@v4` with `10.0.x`. - -**Validation:** - -```powershell -dotnet --version -dotnet build .\bnf_and_pdf.sln -dotnet test .\pdfsharpdslTests\pdfsharpdslTests.csproj -powershell -NoProfile -File .\scripts\coverage.ps1 -``` - -**Handoff:** Record SDK and target framework versions for package validation. - -## TASK-009: Package validation - -**Status:** Blocked by coverage -**Depends on:** TASK-008 - -**Objective:** Validate both configurations, NuGet packages, generator loading in a clean consumer, console samples, and rendering tests. - -**Affected files:** Project packaging metadata, test fixtures, console samples - -**Acceptance criteria:** Debug and Release builds pass, both packages can be packed, a clean `net10.0` consumer loads the generator, and samples/rendering tests pass. - -**Validation:** - -```powershell -dotnet build .\bnf_and_pdf.sln --configuration Debug -dotnet build .\bnf_and_pdf.sln --configuration Release -dotnet pack .\PdfSharpDslCore\PdfSharpDslCore.csproj --configuration Release -dotnet pack .\PdfSharpDslCore.Generator\PdfSharpDslCore.Generator.csproj --configuration Release -dotnet test .\pdfsharpdslTests\pdfsharpdslTests.csproj -``` - -**Handoff:** Record package paths and clean-consumer results for documentation. - -## TASK-010: Documentation - -**Status:** Blocked by coverage -**Depends on:** TASK-009 - -**Objective:** Document the completed migration, framework support, and coverage workflow. - -**Affected files:** `README.md`, `PdfSharpDslCore/Readme.md`, `CHANGELOG.md`, `tasks.md` - -**Acceptance criteria:** The root README links this tracker, supported frameworks and coverage commands are accurate, and the changelog summarizes the migration. - -**Validation:** - -```powershell -powershell -NoProfile -File .\scripts\coverage.ps1 -dotnet build .\bnf_and_pdf.sln --configuration Release -``` - -**Handoff:** Mark the roadmap complete only after all links, commands, and recorded versions are verified. \ No newline at end of file