From 4f02fcfe6d72accae108b57f24df513b0a743c30 Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Fri, 18 Sep 2026 13:41:29 +0200 Subject: [PATCH 1/9] add overage to 90% --- .runsettings | 13 ++ CHANGELOG.md | 3 + pdfsharpdslTests/DrawingValueTests.cs | 90 ++++++++ pdfsharpdslTests/FormulaTests.cs | 88 ++++++- .../ReplayerTests/InstructionRecorderTests.cs | 87 +++++++ pdfsharpdslTests/VariablesDictionaryTests.cs | 84 +++++++ pdfsharpdslTests/VisitorTests.cs | 121 ++++++++++ tasks.md | 218 ++++++++++++++++++ 8 files changed, 703 insertions(+), 1 deletion(-) create mode 100644 .runsettings create mode 100644 pdfsharpdslTests/DrawingValueTests.cs create mode 100644 pdfsharpdslTests/VariablesDictionaryTests.cs create mode 100644 tasks.md diff --git a/.runsettings b/.runsettings new file mode 100644 index 0000000..4e5d087 --- /dev/null +++ b/.runsettings @@ -0,0 +1,13 @@ + + + + + + + cobertura + [PdfSharpDslCore]* + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b7cdfb9..98fe6d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Change log +## Version 1.0.5 (March 3, 2024) +* Update nugets packages and upgrade to .Net 8 + ## Version 1.0.4 (May 19, 2023) * Add callback onNewpage in order to draw a custom template on each page. - Define UDF "__ONNEWPAGE()" in .ipdf file or register it via method 'RegisterCustomUdf(..)' in your dotnet language. diff --git a/pdfsharpdslTests/DrawingValueTests.cs b/pdfsharpdslTests/DrawingValueTests.cs new file mode 100644 index 0000000..f28a17d --- /dev/null +++ b/pdfsharpdslTests/DrawingValueTests.cs @@ -0,0 +1,90 @@ +using PdfSharpCore.Drawing; +using PdfSharpDslCore.Drawing; +using PdfSharpDslCore.Extensions; + +namespace pdfsharpdslTests +{ + public class DrawingValueTests + { + [Fact] + public void OffsetYMovesRectanglesAndPointsWithoutChangingInputs() + { + var rectangle = new XRect(10, 20, 30, 40); + var point = new XPoint(5, 6); + + var movedRectangle = rectangle.OffsetY(7); + var movedPoint = point.OffsetY(8); + + Assert.Equal(20, rectangle.Y); + Assert.Equal(27, movedRectangle.Y); + Assert.Equal(10, movedRectangle.X); + Assert.Equal(6, point.Y); + Assert.Equal(14, movedPoint.Y); + Assert.Equal(5, movedPoint.X); + } + + [Fact] + public void OffsetYMovesPointArraysOnlyWhenNeeded() + { + var points = new[] { new XPoint(1, 2), new XPoint(3, 4) }; + + var unchanged = points.OffsetY(0); + var moved = points.OffsetY(10); + + Assert.Same(points, unchanged); + Assert.NotSame(points, moved); + Assert.Equal(new[] { 12.0, 14.0 }, moved.Select(point => point.Y)); + Assert.Equal(new[] { 1.0, 3.0 }, moved.Select(point => point.X)); + } + + [Fact] + public void TableDefinitionCalculatesColumnDimensionsAndAlignment() + { + var table = new TableDefinition + { + TopMarginOnPageBreak = 12, + ShowHeader = false, + HeaderHeight = 20 + }; + table.Columns.Add(new ColumnDefinition + { + DesiredWidth = 80, + MaxWidth = 50, + Alignment = XStringAlignment.Center + }); + table.Columns.Add(new ColumnDefinition()); + table.Rows.Add(new RowDefinition + { + DesiredHeight = 15, + MaxHeight = 20, + Data = new[] { "value" } + }); + + Assert.Equal(80, table.ColWidth(0)); + 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(12, table.TopMarginOnPageBreak); + Assert.False(table.ShowHeader); + Assert.Equal(20, table.HeaderHeight); + Assert.Equal(15, table.Rows[0].DesiredHeight); + Assert.Equal(20, table.Rows[0].MaxHeight); + Assert.Equal("value", table.Rows[0].Data[0]); + } + + [Fact] + public void DrawingResultStoresRectangleAndPageOffset() + { + var rectangle = new XRect(1, 2, 3, 4); + var result = new DrawingResult + { + DrawingRect = rectangle, + PageOffsetY = 25 + }; + + Assert.Equal(rectangle, result.DrawingRect); + Assert.Equal(25, result.PageOffsetY); + } + } +} \ No newline at end of file diff --git a/pdfsharpdslTests/FormulaTests.cs b/pdfsharpdslTests/FormulaTests.cs index 3449c2d..301f334 100644 --- a/pdfsharpdslTests/FormulaTests.cs +++ b/pdfsharpdslTests/FormulaTests.cs @@ -1,4 +1,5 @@ -using PdfSharpDslCore.Parser;/**/ +using PdfSharpDslCore.Evaluation; +using PdfSharpDslCore.Parser;/**/ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -66,5 +67,90 @@ public void ConvertTests() Assert.Equal(1.0, Convert.ToDouble(true)); Assert.Equal(0.0, Convert.ToDouble(false)); } + + [Theory] + [InlineData((int)BinaryOperation.Sub, 1.0)] + [InlineData((int)BinaryOperation.Mul, 30.0)] + [InlineData((int)BinaryOperation.Div, 1.2)] + [InlineData((int)BinaryOperation.Mod, 1.0)] + public void BinaryNumericOperations(int operation, double expected) + { + var evaluation = new BinaryEvaluation( + new ConstantEvaluation(6), + new ConstantEvaluation(5), + (BinaryOperation)operation); + + Assert.Equal(expected, evaluation.Value); + } + + [Theory] + [InlineData((int)BinaryOperation.And, true, false, false)] + [InlineData((int)BinaryOperation.Or, false, true, true)] + [InlineData((int)BinaryOperation.Superior, 2, 1, true)] + [InlineData((int)BinaryOperation.SuperiorOrEquals, 2, 2, true)] + [InlineData((int)BinaryOperation.Inferior, 1, 2, true)] + [InlineData((int)BinaryOperation.InferiorOrEquals, 2, 2, true)] + [InlineData((int)BinaryOperation.Equals, "same", "same", true)] + [InlineData((int)BinaryOperation.NotEquals, "left", "right", true)] + public void BinaryBooleanOperations(int operation, object left, object right, bool expected) + { + var evaluation = new BinaryEvaluation( + new ConstantEvaluation(left), + new ConstantEvaluation(right), + (BinaryOperation)operation); + + Assert.Equal(expected, evaluation.Value); + } + + [Fact] + public void BinaryOperationsReportInvalidOperands() + { + var nullOperand = new BinaryEvaluation( + new ConstantEvaluation(null!), + new ConstantEvaluation(1), + BinaryOperation.Add); + var unsupportedStringOperation = new BinaryEvaluation( + new ConstantEvaluation("left"), + new ConstantEvaluation("right"), + BinaryOperation.Sub); + var invalidComparison = new BinaryEvaluation( + new ConstantEvaluation("left"), + new ConstantEvaluation(1), + BinaryOperation.Superior); + + Assert.Throws(() => nullOperand.Value); + Assert.Throws(() => unsupportedStringOperation.Value); + Assert.Throws(() => invalidComparison.Value); + } + + [Theory] + [InlineData((int)BinaryOperation.Add, 5, 5.0)] + [InlineData((int)BinaryOperation.Sub, 5, -5.0)] + [InlineData((int)BinaryOperation.Add, "value", "value")] + [InlineData((int)BinaryOperation.Sub, "value", "-value")] + public void UnaryOperations(int operation, object value, object expected) + { + var evaluation = new UnaryEvaluation(new ConstantEvaluation(value), (BinaryOperation)operation); + + Assert.Equal(expected, evaluation.Value); + } + + [Fact] + public void UnaryOperationsReportInvalidOperands() + { + var nullOperand = new UnaryEvaluation(new ConstantEvaluation(null!), BinaryOperation.Add); + var unsupportedOperation = new UnaryEvaluation(new ConstantEvaluation(1), BinaryOperation.Mul); + + Assert.Throws(() => nullOperand.Value); + Assert.Throws(() => unsupportedOperation.Value); + } + + [Fact] + public void MissingVariableThrows() + { + var evaluation = new VariableEvaluation("missing", new Dictionary()); + + Assert.Throws(() => evaluation.Value); + } } } diff --git a/pdfsharpdslTests/ReplayerTests/InstructionRecorderTests.cs b/pdfsharpdslTests/ReplayerTests/InstructionRecorderTests.cs index e34d28a..2e55e26 100644 --- a/pdfsharpdslTests/ReplayerTests/InstructionRecorderTests.cs +++ b/pdfsharpdslTests/ReplayerTests/InstructionRecorderTests.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using Irony; +using Microsoft.Extensions.Logging; using Xunit; namespace pdfsharpdslTests.ReplayerTests @@ -147,6 +148,92 @@ public void RecorderTests_block_with_offsetY() Assert.Equal(new XRect(0,100, 50, 100), block1.Rect); } + [Fact] + public void InstructionActionExecutesWithOffsetAndExposesMetadata() + { + var rectangle = new XRect(1, 2, 3, 4); + double? appliedOffset = null; + var instruction = new InstructionAction(offset => appliedOffset = offset, rectangle, "action"); + + var result = instruction.Draw(defaultDrawerMock().Object, 12, 99); + + Assert.Equal(rectangle, instruction.Rect); + Assert.Equal("action", instruction.Name); + Assert.Equal(12, appliedOffset); + Assert.Equal(0, result); + } + + [Fact] + public void RecorderRootRejectsInstructionsAndBlockMetadataCanBeCleared() + { + var recorder = new BlocksRecorder(); + var instruction = new DummyInstruction(new XRect(0, 0, 10, 10)); + + Assert.False(recorder.CanPushInstruction); + Assert.Throws(() => recorder.CurrentBlock.PushInstruction(instruction)); + + var block = recorder.OpenBlock("named", 15, false, 4); + block.PushInstruction(instruction, false); + + Assert.True(recorder.CanPushInstruction); + Assert.Equal("named", block.Name); + Assert.Equal(15, block.OffsetY); + Assert.NotNull(block.Parent); + Assert.True(block.Rect.IsEmpty); + Assert.Single(block.Instructions); + + block.Clear(); + Assert.Empty(block.Instructions); + } + + [Fact] + public void NestedBlockMovesToNextPage() + { + var logger = new Mock(); + logger.Setup(x => x.IsEnabled(LogLevel.Debug)).Returns(true); + var drawer = defaultDrawerMock(); + 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)); + 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.True(pageOffset > 0); + } + + [Fact] + public void OversizedNestedBlockCannotBePrintedEntirely() + { + var drawer = defaultDrawerMock(); + 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))); + + Assert.Throws(() => outer.Draw(drawer.Object, 0, 0)); + } + + [Fact] + 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)); + block.PushInstruction(new InstructionAction( + _ => block.PushInstruction(extraInstruction), + new XRect(0, 0, 10, 10), + "mutating")); + + block.Draw(drawer.Object, 0, 0); + + Assert.Single(block.Instructions); + } + private static void AddInstructions(IInstructionBlock block, int count, int height) { var r = new XRect(0, 0, 50, height); diff --git a/pdfsharpdslTests/VariablesDictionaryTests.cs b/pdfsharpdslTests/VariablesDictionaryTests.cs new file mode 100644 index 0000000..b1531e6 --- /dev/null +++ b/pdfsharpdslTests/VariablesDictionaryTests.cs @@ -0,0 +1,84 @@ +using System.Collections; +using PdfSharpDslCore.Parser; + +namespace pdfsharpdslTests +{ + public class VariablesDictionaryTests + { + [Fact] + public void DictionarySupportsMutationAndSystemVariables() + { + var requestedSystemVariables = new List(); + var variables = new VariablesDictionary(name => + { + requestedSystemVariables.Add(name); + return name == "PAGEWIDTH" ? 595 : 842; + }); + + variables.Add("FIRST", 1); + variables.Add(new KeyValuePair("SECOND", 2)); + + Assert.False(variables.IsReadOnly); + Assert.Equal(2, variables.Count); + Assert.Contains("FIRST", variables.Keys); + Assert.Contains(2, variables.Values); + Assert.True(variables.ContainsKey("SECOND")); + Assert.Equal(1, variables["FIRST"]); + Assert.Null(variables["MISSING"]); + Assert.Equal(595, variables["PAGEWIDTH"]); + Assert.Equal(842, variables["PAGEHEIGHT"]); + Assert.Equal(new[] { "PAGEWIDTH", "PAGEHEIGHT" }, requestedSystemVariables); + Assert.Throws(() => variables["FIRST"] = 3); + + Assert.True(variables.Remove("FIRST")); + Assert.False(variables.Remove("MISSING")); + variables.Clear(); + Assert.Empty(variables.Keys); + } + + [Fact] + public void SaveAndRestorePreserveOuterScope() + { + var variables = new VariablesDictionary(_ => 0); + variables.Add("VALUE", "outer"); + + variables.SaveVariables(); + variables.Add("VALUE", "inner"); + variables.Add("INNER_ONLY", true); + + Assert.Equal("inner", variables["VALUE"]); + variables.RestoreVariables(); + Assert.Equal("outer", variables["VALUE"]); + Assert.False(variables.ContainsKey("INNER_ONLY")); + } + + [Fact] + public void GlobalScopeDisablesSaveAndRestore() + { + var variables = new VariablesDictionary(_ => 0) + { + GlobalScope = true + }; + variables.Add("VALUE", "outer"); + + variables.SaveVariables(); + variables.Add("VALUE", "global"); + variables.RestoreVariables(); + + Assert.Equal("global", variables["VALUE"]); + } + + [Fact] + public void UnsupportedCollectionMembersThrow() + { + var variables = new VariablesDictionary(_ => 0); + var item = new KeyValuePair("VALUE", 1); + + Assert.Throws(() => variables.Contains(item)); + Assert.Throws(() => variables.CopyTo(new[] { item }, 0)); + Assert.Throws(() => variables.Remove(item)); + Assert.Throws(() => variables.GetEnumerator()); + Assert.Throws(() => ((IEnumerable)variables).GetEnumerator()); + } + } +} \ No newline at end of file diff --git a/pdfsharpdslTests/VisitorTests.cs b/pdfsharpdslTests/VisitorTests.cs index 02dfacb..78344a2 100644 --- a/pdfsharpdslTests/VisitorTests.cs +++ b/pdfsharpdslTests/VisitorTests.cs @@ -102,5 +102,126 @@ public void TestConditionEvaluator(string file) Assert.Equal("OK", udF.Value[0]); } } + + [Fact] + public void DrawExecutesViewSizeTextWidthAndDebugOptions() + { + var tree = ParseText("VIEWSIZE 100,140;TEXT 10,20 MaxWidth=30 Text=\"hello\";DEBUGOPTIONS DEBUG_TEXT, DEBUG_RECT, DEBUG_ROWTEMPLATE, DEBUG_RULE, DEBUG_ALL, UNKNOWN;"); + var drawer = new Mock(); + drawer.SetupProperty(x => x.DebugOptions); + + new PdfDrawerVisitor().Draw(drawer.Object, tree); + + drawer.Verify(x => x.SetViewSize(100, 140), Times.Once); + drawer.Verify(x => x.DrawText("hello", 10, 20, 30, null), Times.Once); + Assert.Equal( + DebugOptions.DebugText | DebugOptions.DebugRect | DebugOptions.DebugRowTemplate | DebugOptions.DebugRule | DebugOptions.DebugAll, + drawer.Object.DebugOptions); + } + + [Fact] + public void DrawResolvesPageSystemVariables() + { + var tree = ParseText("SET VAR WIDTH=$PAGEWIDTH;SET VAR HEIGHT=$PAGEHEIGHT;"); + var drawer = new Mock(); + drawer.SetupGet(x => x.PageWidth).Returns(612); + drawer.SetupGet(x => x.PageHeight).Returns(792); + var visitor = new InspectablePdfDrawerVisitor(); + + visitor.Draw(drawer.Object, tree); + + Assert.Equal(612.0, visitor.Vars["WIDTH"]); + Assert.Equal(792.0, visitor.Vars["HEIGHT"]); + } + + [Fact] + public void CustomUdfCanFallBackToDslBodyOrOverrideIt() + { + var fallbackTree = ParseText("UDF SAMPLE(X) LINE $X,0,$X,1; ENDUDF CALL SAMPLE(3);"); + var overrideTree = ParseText("CALL CUSTOM(5);"); + var drawer = new Mock(); + var visitor = new InspectablePdfDrawerVisitor(); + string[]? parameterNames = null; + object?[]? parameterValues = null; + visitor.RegisterCustomUdf("sample", (_, names, values) => + { + parameterNames = names; + parameterValues = values; + return false; + }); + visitor.RegisterCustomUdf("custom", (_, _, _) => false); + visitor.RegisterCustomUdf("CUSTOM", (_, _, values) => + { + parameterValues = values; + return true; + }); + + visitor.Draw(drawer.Object, fallbackTree); + visitor.Draw(drawer.Object, overrideTree); + + Assert.Equal(new[] { "X" }, parameterNames); + Assert.Equal(5.0, Assert.Single(parameterValues!)); + drawer.Verify(x => x.DrawLine(3, 0, 3, 1), Times.Once); + } + + [Fact] + public void CustomUdfErrorsAreReportedAsParserErrors() + { + var customFailureTree = ParseText("CALL CUSTOM();"); + var missingTree = ParseText("CALL MISSING();"); + var drawer = new Mock(); + var visitor = new InspectablePdfDrawerVisitor(); + visitor.RegisterCustomUdf("CUSTOM", (_, _, _) => throw new InvalidOperationException("failure")); + + var customError = Assert.Throws(() => visitor.Draw(drawer.Object, customFailureTree)); + var missingError = Assert.Throws(() => visitor.Draw(drawer.Object, missingTree)); + + Assert.IsType(customError.InnerException); + Assert.Contains("MISSING", missingError.Message); + } + + [Fact] + 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 }); + var visitor = new InspectablePdfDrawerVisitor(); + + visitor.Draw(drawer.Object, tree); + + drawer.Verify(x => x.BeginIterationTemplate(2), Times.Once); + drawer.Verify(x => x.BeginDrawRowTemplate("row", 0, 12, 5), Times.Once); + drawer.Verify(x => x.BeginDrawRowTemplate("row", 1, 34, 5), Times.Once); + drawer.Verify(x => x.EndIterationTemplate(42), Times.Once); + drawer.Verify(x => x.DrawLine(0, 0, 10, 10), Times.Exactly(2)); + Assert.Equal(42.0, visitor.Vars["LASTTEMPLATEHEIGHT"]); + Assert.False(visitor.Vars.ContainsKey("ROWINDEX")); + } + + [Fact] + public void OnNewPageUdfPersistsGlobalVariables() + { + var tree = ParseText("UDF __ONNEWPAGE() SET VAR SAVEDPAGE=$PAGEINDEX; ENDUDF"); + var drawer = new Mock(); + Action? onNewPage = null; + drawer.Setup(x => x.RegisterOnNewPage(It.IsAny>())) + .Callback>(callback => onNewPage = callback); + var visitor = new InspectablePdfDrawerVisitor(); + + visitor.Draw(drawer.Object, tree); + Assert.NotNull(onNewPage); + onNewPage!(4); + + Assert.Equal(4, visitor.Vars["PAGEINDEX"]); + Assert.Equal(4, visitor.Vars["SAVEDPAGE"]); + } + + private sealed class InspectablePdfDrawerVisitor : PdfDrawerVisitor + { + public IDictionary Vars => Variables; + } } } diff --git a/tasks.md b/tasks.md new file mode 100644 index 0000000..23f1dc6 --- /dev/null +++ b/tasks.md @@ -0,0 +1,218 @@ +# .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 From 3938138912864014d17f4173056a08ce59376239 Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Fri, 18 Sep 2026 14:21:57 +0200 Subject: [PATCH 2/9] migration dotnet 10 + coverage up to 90% --- .github/workflows/build.yml | 4 +- .github/workflows/release-package.yml | 4 +- CHANGELOG.md | 6 + PdfSharpDslConsole/PdfSharpDslConsole.csproj | 4 +- .../PdfSharpDslCore.Generator.csproj | 20 ++-- PdfSharpDslCore/Drawing/DrawingHelper.cs | 4 +- PdfSharpDslCore/Evaluation/Evaluator.cs | 3 + PdfSharpDslCore/Parser/PdfDrawerVisitor.cs | 4 +- PdfSharpDslCore/Parser/PdfVisitor.cs | 6 +- PdfSharpDslCore/PdfSharpDslCore.csproj | 3 +- PdfSharpDslCore/Readme.md | 4 + README.md | 15 +++ global.json | 2 +- pdfsharpdslTests/DrawingValueTests.cs | 31 +++++ pdfsharpdslTests/FormulaTests.cs | 15 ++- pdfsharpdslTests/PdfDocumentDrawerTests.cs | 36 ++++++ .../SourceGenerator/SourceGeneratorTests.cs | 88 +++++++++++++- pdfsharpdslTests/VisitorTests.cs | 113 +++++++++++++++++- pdfsharpdslTests/pdfsharpdslTests.csproj | 12 +- scripts/coverage.ps1 | 60 ++++++++++ 20 files changed, 399 insertions(+), 35 deletions(-) create mode 100644 pdfsharpdslTests/PdfDocumentDrawerTests.cs create mode 100644 scripts/coverage.ps1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25f43e7..a3ddfb3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,9 +17,9 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index bbecc78..94a0a23 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -13,9 +13,9 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore diff --git a/CHANGELOG.md b/CHANGELOG.md index 98fe6d7..08713a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change log +## Unreleased +* 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. +* Updated dependencies and resolved known ImageSharp and runtime package advisories. + ## Version 1.0.5 (March 3, 2024) * Update nugets packages and upgrade to .Net 8 diff --git a/PdfSharpDslConsole/PdfSharpDslConsole.csproj b/PdfSharpDslConsole/PdfSharpDslConsole.csproj index 06cf314..621402a 100644 --- a/PdfSharpDslConsole/PdfSharpDslConsole.csproj +++ b/PdfSharpDslConsole/PdfSharpDslConsole.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable 0.1.0 @@ -14,7 +14,7 @@ - + diff --git a/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj b/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj index 410878f..f83c090 100644 --- a/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj +++ b/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj @@ -7,11 +7,10 @@ Pdf Generation using source generation - 1.0.0 + 1.0.2 Pierrick Gourlain https://github.com/pgourlain/bnf_and_pdf - Icon.jpg LICENSE.md false false @@ -20,18 +19,24 @@ - + + - - - + + + + + + + + @@ -39,6 +44,7 @@ + @@ -52,7 +58,7 @@ - + diff --git a/PdfSharpDslCore/Drawing/DrawingHelper.cs b/PdfSharpDslCore/Drawing/DrawingHelper.cs index 78c39f7..0956954 100644 --- a/PdfSharpDslCore/Drawing/DrawingHelper.cs +++ b/PdfSharpDslCore/Drawing/DrawingHelper.cs @@ -24,7 +24,7 @@ public static XRect RectFromStringFormat(XRect r, XSize textSize, XStringFormat case XStringAlignment.Near: break; case XStringAlignment.Far: - result.Offset(r.Right - textSize.Width, 0); + result.Offset(r.Width - textSize.Width, 0); break; } @@ -36,7 +36,7 @@ public static XRect RectFromStringFormat(XRect r, XSize textSize, XStringFormat case XLineAlignment.Near: break; case XLineAlignment.Far: - result.Offset(0, r.Bottom - textSize.Height); + result.Offset(0, r.Height - textSize.Height); break; } diff --git a/PdfSharpDslCore/Evaluation/Evaluator.cs b/PdfSharpDslCore/Evaluation/Evaluator.cs index a08fa7f..3e1c2f9 100644 --- a/PdfSharpDslCore/Evaluation/Evaluator.cs +++ b/PdfSharpDslCore/Evaluation/Evaluator.cs @@ -79,6 +79,9 @@ private IEvaluation PerformEvaluate(ParseTreeNode node, IDictionary": op = BinaryOperation.Superior; break; diff --git a/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs b/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs index 12e0764..27c7f38 100644 --- a/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs +++ b/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs @@ -294,7 +294,8 @@ protected override void ExecuteImage(IPdfDocumentDrawer drawer, ParseTreeNode lo { //try to parse unit and cropping unit = unitNode.Term.Name; - crop = cropNode?.ChildNodes.Count > 0; + crop = cropNode?.ChildNodes.Any(x => + string.Equals(x.Token?.Text, "crop", StringComparison.OrdinalIgnoreCase)) == true; } XImage image; @@ -471,6 +472,7 @@ protected override void ExecuteDebugOptions(IPdfDocumentDrawer state, IEnumerabl "DEBUG_TEXT" => DebugOptions.DebugText, "DEBUG_RECT" => DebugOptions.DebugRect, "DEBUG_ROWTEMPLATE" => DebugOptions.DebugRowTemplate, + "DEBUG_IMAGE" => DebugOptions.DebugImage, "DEBUG_RULE" => DebugOptions.DebugRule, "DEBUG_ALL" => DebugOptions.DebugAll, _ => DebugOptions.None diff --git a/PdfSharpDslCore/Parser/PdfVisitor.cs b/PdfSharpDslCore/Parser/PdfVisitor.cs index 8fb56cd..4a2dee6 100644 --- a/PdfSharpDslCore/Parser/PdfVisitor.cs +++ b/PdfSharpDslCore/Parser/PdfVisitor.cs @@ -314,9 +314,11 @@ protected void ExecuteUdfByName(TState state, string fnName, ParseTreeNode? argu { defArgs = defNode.ChildNode("UdfArgumentslist")!; defBody = defNode.ChildNode("UdfBlock")?.ChildNode("EmbbededSmtList")!; - if (defArgs!= null && arguments!=null && defArgs.ChildNodes.Count != arguments.ChildNodes.Count) + var expectedArgumentCount = defArgs?.ChildNodes.Count ?? 0; + var providedArgumentCount = arguments?.ChildNodes.Count ?? 0; + if (expectedArgumentCount != providedArgumentCount) { - throw new PdfParserException($"UDF '{fnName}' arguments count not match, provided ${arguments.ChildNodes.Count}, expected ${defArgs.ChildNodes.Count}."); + throw new PdfParserException($"UDF '{fnName}' arguments count does not match, provided {providedArgumentCount}, expected {expectedArgumentCount}."); } if (defArgs == null) diff --git a/PdfSharpDslCore/PdfSharpDslCore.csproj b/PdfSharpDslCore/PdfSharpDslCore.csproj index 0cd7958..354a1cc 100644 --- a/PdfSharpDslCore/PdfSharpDslCore.csproj +++ b/PdfSharpDslCore/PdfSharpDslCore.csproj @@ -24,9 +24,10 @@ - + + diff --git a/PdfSharpDslCore/Readme.md b/PdfSharpDslCore/Readme.md index a7c4494..f4ffd0b 100644 --- a/PdfSharpDslCore/Readme.md +++ b/PdfSharpDslCore/Readme.md @@ -3,6 +3,10 @@ Package to print PDF using a specific DSL, using Irony.Net and PdfSharpCore +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 repository enforces at least 90% line coverage for `PdfSharpDslCore` through `scripts\coverage.ps1`. + # How to Goto [https://github.com/pgourlain/bnf_and_pdf](https://github.com/pgourlain/bnf_and_pdf) for example and usage diff --git a/README.md b/README.md index 87e75f1..59bb7b8 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,21 @@ 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/) +## Current support + +- `PdfSharpDslCore` and `PdfSharpDslCore.Generator` target `netstandard2.0`. +- `PdfSharpDslConsole` and the test project target `net10.0`. +- The repository is pinned to SDK `10.0.400` in `global.json`. +- The generator package includes its analyzer dependencies and supports clean NuGet consumer builds. + +Run the full test and core coverage gate with: + +```powershell +powershell -NoProfile -File .\scripts\coverage.ps1 +``` + +The gate requires at least 90% core line coverage. See [tasks.md](tasks.md) for the migration tracker and verified results. + # Example diff --git a/global.json b/global.json index 1b5ac1d..38c4f3c 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "10.0.400", "rollForward": "latestFeature" } } \ No newline at end of file diff --git a/pdfsharpdslTests/DrawingValueTests.cs b/pdfsharpdslTests/DrawingValueTests.cs index f28a17d..740684b 100644 --- a/pdfsharpdslTests/DrawingValueTests.cs +++ b/pdfsharpdslTests/DrawingValueTests.cs @@ -65,6 +65,8 @@ public void TableDefinitionCalculatesColumnDimensionsAndAlignment() Assert.Equal(50, table.ColMaxWidth(0, 100)); Assert.Equal(40, table.ColMaxWidth(1, 40)); Assert.Equal(XStringAlignment.Center, table.Alignment(0)); + Assert.Equal(50, table.Columns[0].DrawWidth); + Assert.Equal(0, table.Columns[1].DrawWidth); Assert.Equal(12, table.TopMarginOnPageBreak); Assert.False(table.ShowHeader); Assert.Equal(20, table.HeaderHeight); @@ -86,5 +88,34 @@ public void DrawingResultStoresRectangleAndPageOffset() Assert.Equal(rectangle, result.DrawingRect); Assert.Equal(25, result.PageOffsetY); } + + [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 + }); + + Assert.Equal(new XRect(45, 40, 30, 10), centered); + Assert.Equal(new XRect(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); + + Assert.Equal(bounds, result); + } } } \ No newline at end of file diff --git a/pdfsharpdslTests/FormulaTests.cs b/pdfsharpdslTests/FormulaTests.cs index 301f334..50deefc 100644 --- a/pdfsharpdslTests/FormulaTests.cs +++ b/pdfsharpdslTests/FormulaTests.cs @@ -1,4 +1,6 @@ -using PdfSharpDslCore.Evaluation; +using Moq; +using PdfSharpDslCore.Drawing; +using PdfSharpDslCore.Evaluation; using PdfSharpDslCore.Parser;/**/ using System; using System.Collections.Generic; @@ -152,5 +154,16 @@ public void MissingVariableThrows() Assert.Throws(() => evaluation.Value); } + + [Fact] + public void ModuloOperatorIsEvaluatedFromParsedDsl() + { + var tree = ParseText("SET VAR X=7%3;"); + var visitor = new PdfDrawerForTestsVisitor(); + + visitor.Draw(Mock.Of(), tree); + + Assert.Equal(1.0, visitor.Vars["X"]); + } } } diff --git a/pdfsharpdslTests/PdfDocumentDrawerTests.cs b/pdfsharpdslTests/PdfDocumentDrawerTests.cs new file mode 100644 index 0000000..685f1ac --- /dev/null +++ b/pdfsharpdslTests/PdfDocumentDrawerTests.cs @@ -0,0 +1,36 @@ +using PdfSharpCore; +using PdfSharpCore.Pdf; +using PdfSharpDslCore.Drawing; + +namespace pdfsharpdslTests +{ + public class PdfDocumentDrawerTests + { + [Fact] + public void NewPageUsesDefaultsAndPersistsExplicitSettings() + { + using var document = new PdfDocument(); + using var drawer = new PdfDocumentDrawer(document); + 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); + + drawer.NewPage(PageSize.Letter, PageOrientation.Landscape); + Assert.Equal(PageSize.Letter, drawer.CurrentPage.Size); + Assert.Equal(PageOrientation.Landscape, drawer.CurrentPage.Orientation); + + drawer.NewPage(); + Assert.Equal(PageSize.Letter, drawer.CurrentPage.Size); + Assert.Equal(PageOrientation.Landscape, drawer.CurrentPage.Orientation); + Assert.Equal(new[] { 2, 3 }, pageNumbers); + + drawer.UnRegisterOnNewPage(callback); + drawer.NewPage(); + Assert.Equal(new[] { 2, 3 }, pageNumbers); + } + } +} \ No newline at end of file diff --git a/pdfsharpdslTests/SourceGenerator/SourceGeneratorTests.cs b/pdfsharpdslTests/SourceGenerator/SourceGeneratorTests.cs index d1c0ebb..a64dc11 100644 --- a/pdfsharpdslTests/SourceGenerator/SourceGeneratorTests.cs +++ b/pdfsharpdslTests/SourceGenerator/SourceGeneratorTests.cs @@ -1,11 +1,12 @@ -using PdfSharpDslCore.Generator; -using System; -using System.Collections.Generic; +using Microsoft.CodeAnalysis; +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; -using System.Linq; -using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; namespace pdfsharpdslTests.SourceGenerator { @@ -22,5 +23,80 @@ public void TestSourceGeneration(string file) Assert.NotNull(result); } + + [Fact] + public void GeneratorProducesCompilableSourceFromTaggedAdditionalFile() + { + var source = CSharpSyntaxTree.ParseText("namespace Consumer { internal class Marker { } }"); + var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) + .Split(Path.PathSeparator) + .Append(typeof(IPdfDocumentDrawer).Assembly.Location) + .Append(typeof(XColor).Assembly.Location) + .Distinct() + .Select(path => MetadataReference.CreateFromFile(path)); + var compilation = CSharpCompilation.Create( + "Consumer", + new[] { source }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var additionalFile = new InMemoryAdditionalText("sample.txt", "NEWPAGE;"); + GeneratorDriver driver = CSharpGeneratorDriver.Create( + new[] { new DslGenerator() }, + new[] { additionalFile }, + (CSharpParseOptions)source.Options, + new PdfDslOptionsProvider()); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); + + Assert.Empty(generatorDiagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)); + Assert.Equal(2, outputCompilation.SyntaxTrees.Count()); + Assert.Empty(outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)); + } + + private sealed class InMemoryAdditionalText : AdditionalText + { + private readonly SourceText _text; + + public InMemoryAdditionalText(string path, string text) + { + Path = path; + _text = SourceText.From(text, Encoding.UTF8); + } + + public override string Path { get; } + + public override SourceText GetText(CancellationToken cancellationToken = default) => _text; + } + + private sealed class PdfDslOptionsProvider : AnalyzerConfigOptionsProvider + { + private static readonly AnalyzerConfigOptions Empty = new TestAnalyzerConfigOptions(false); + private static readonly AnalyzerConfigOptions PdfDsl = new TestAnalyzerConfigOptions(true); + + public override AnalyzerConfigOptions GlobalOptions => Empty; + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => Empty; + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => PdfDsl; + } + + private sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions + { + private readonly bool _isPdfDsl; + + public TestAnalyzerConfigOptions(bool isPdfDsl) => _isPdfDsl = isPdfDsl; + + public override bool TryGetValue(string key, out string value) + { + if (_isPdfDsl && key == "build_metadata.additionalfiles.IsPdfSharpDsl") + { + value = "true"; + return true; + } + + value = string.Empty; + return false; + } + } } } diff --git a/pdfsharpdslTests/VisitorTests.cs b/pdfsharpdslTests/VisitorTests.cs index 78344a2..1243142 100644 --- a/pdfsharpdslTests/VisitorTests.cs +++ b/pdfsharpdslTests/VisitorTests.cs @@ -106,7 +106,7 @@ public void TestConditionEvaluator(string file) [Fact] public void DrawExecutesViewSizeTextWidthAndDebugOptions() { - var tree = ParseText("VIEWSIZE 100,140;TEXT 10,20 MaxWidth=30 Text=\"hello\";DEBUGOPTIONS DEBUG_TEXT, DEBUG_RECT, DEBUG_ROWTEMPLATE, DEBUG_RULE, DEBUG_ALL, UNKNOWN;"); + var tree = ParseText("VIEWSIZE 100,140;TEXT 10,20 MaxWidth=30 Text=\"hello\";DEBUGOPTIONS DEBUG_TEXT, DEBUG_RECT, DEBUG_ROWTEMPLATE, DEBUG_IMAGE, DEBUG_RULE, DEBUG_ALL, UNKNOWN;"); var drawer = new Mock(); drawer.SetupProperty(x => x.DebugOptions); @@ -115,7 +115,7 @@ public void DrawExecutesViewSizeTextWidthAndDebugOptions() drawer.Verify(x => x.SetViewSize(100, 140), Times.Once); drawer.Verify(x => x.DrawText("hello", 10, 20, 30, null), Times.Once); Assert.Equal( - DebugOptions.DebugText | DebugOptions.DebugRect | DebugOptions.DebugRowTemplate | DebugOptions.DebugRule | DebugOptions.DebugAll, + DebugOptions.DebugText | DebugOptions.DebugRect | DebugOptions.DebugRowTemplate | DebugOptions.DebugImage | DebugOptions.DebugRule | DebugOptions.DebugAll, drawer.Object.DebugOptions); } @@ -134,6 +134,66 @@ public void DrawResolvesPageSystemVariables() Assert.Equal(792.0, visitor.Vars["HEIGHT"]); } + [Fact] + public void ImageStatementsPreserveDimensionsUnitsAndCropMode() + { + const string imageData = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; + var tree = ParseText( + $"IMAGE 1,2 Data=\"data:image/png;base64,{imageData}\";" + + $"IMAGE 3,4,30,40 pixel crop Data=\"{imageData}\";" + + $"IMAGE 5,6,70,80 point fit Data=\"{imageData}\";"); + 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())) + .Callback((_, x, y, width, height, pixel, crop) => + calls.Add((x, y, width, height, pixel, crop))); + + new PdfDrawerVisitor().Draw(drawer.Object, tree); + + Assert.Equal((1, 2, null, null, false, false), calls[0]); + Assert.Equal((3, 4, 30, 40, true, true), calls[1]); + Assert.Equal((5, 6, 70, 80, false, false), calls[2]); + } + + [Fact] + public void TableRowTemplateBuildsRowsWidthsAndPadding() + { + var tree = ParseText( + "TABLE 20,30 " + + "HEAD " + + "COL Width=40 MaxWidth=30 \"A\"; " + + "COL Width=auto MaxWidth=100 \"B\"; " + + "ENDHEAD " + + "ROWTEMPLATE 2 " + + "COL $ROWINDEX; " + + "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); + + Assert.NotNull(capturedTable); + Assert.Equal(2, capturedTable.Columns.Count); + Assert.Equal(40, capturedTable.Columns[0].DesiredWidth); + Assert.Equal(30, capturedTable.Columns[0].MaxWidth); + Assert.Null(capturedTable.Columns[1].DesiredWidth); + Assert.Equal(100, capturedTable.Columns[1].MaxWidth); + Assert.Equal(new[] { "0", string.Empty }, capturedTable.Rows[0].Data); + Assert.Equal(new[] { "1", string.Empty }, capturedTable.Rows[1].Data); + Assert.False(visitor.Vars.ContainsKey("ROWINDEX")); + } + [Fact] public void CustomUdfCanFallBackToDslBodyOrOverrideIt() { @@ -180,6 +240,55 @@ public void CustomUdfErrorsAreReportedAsParserErrors() Assert.Contains("MISSING", missingError.Message); } + [Fact] + public void NestedDslUdfsRestoreOuterScopes() + { + var tree = ParseText( + "SET VAR VALUE=10; " + + "UDF INNER(VALUE) SET VAR INNERONLY=99; LINE $VALUE,$INNERONLY,$VALUE,$INNERONLY; ENDUDF " + + "UDF OUTER(VALUE) SET VAR OUTERONLY=20; CALL INNER(30); LINE $VALUE,$OUTERONLY,$VALUE,$OUTERONLY; ENDUDF " + + "CALL OUTER(40); LINE $VALUE,0,$VALUE,0;"); + var drawer = new Mock(); + var visitor = new InspectablePdfDrawerVisitor(); + + visitor.Draw(drawer.Object, tree); + + drawer.Verify(x => x.DrawLine(30, 99, 30, 99), Times.Once); + drawer.Verify(x => x.DrawLine(40, 20, 40, 20), Times.Once); + drawer.Verify(x => x.DrawLine(10, 0, 10, 0), Times.Once); + Assert.Equal(10.0, visitor.Vars["VALUE"]); + Assert.False(visitor.Vars.ContainsKey("INNERONLY")); + Assert.False(visitor.Vars.ContainsKey("OUTERONLY")); + } + + [Fact] + public void DslUdfFailureRestoresOuterScope() + { + var tree = ParseText( + "SET VAR VALUE=1; " + + "UDF FAIL(VALUE) SET VAR TEMP=2; LINE $MISSING,0,0,0; ENDUDF " + + "CALL FAIL(9);"); + var visitor = new InspectablePdfDrawerVisitor(); + + Assert.Throws(() => visitor.Draw(Mock.Of(), tree)); + + Assert.Equal(1.0, visitor.Vars["VALUE"]); + Assert.False(visitor.Vars.ContainsKey("TEMP")); + } + + [Theory] + [InlineData("UDF NEEDS(X) LINE $X,0,0,0; ENDUDF CALL NEEDS();")] + [InlineData("UDF NONE() LINE 0,0,0,0; ENDUDF CALL NONE(1);")] + public void DslUdfRejectsZeroArgumentCountMismatches(string input) + { + var tree = ParseText(input); + var visitor = new InspectablePdfDrawerVisitor(); + + var error = Assert.Throws(() => visitor.Draw(Mock.Of(), tree)); + + Assert.Contains("arguments count", error.Message); + } + [Fact] public void RowTemplateTracksOffsetsAndFinalHeight() { diff --git a/pdfsharpdslTests/pdfsharpdslTests.csproj b/pdfsharpdslTests/pdfsharpdslTests.csproj index c27762a..c721efb 100644 --- a/pdfsharpdslTests/pdfsharpdslTests.csproj +++ b/pdfsharpdslTests/pdfsharpdslTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -11,14 +11,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/scripts/coverage.ps1 b/scripts/coverage.ps1 new file mode 100644 index 0000000..d256cb5 --- /dev/null +++ b/scripts/coverage.ps1 @@ -0,0 +1,60 @@ +[CmdletBinding()] +param( + [ValidateSet("Debug", "Release")] + [string]$Configuration = "Debug", + + [ValidateRange(0, 1)] + [double]$MinimumLineRate = 0.90 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$testProject = Join-Path $repositoryRoot "pdfsharpdslTests\pdfsharpdslTests.csproj" +$settingsFile = Join-Path $repositoryRoot ".runsettings" +$resultsDirectory = Join-Path $repositoryRoot "artifacts\coverage" + +if (Test-Path $resultsDirectory) { + Remove-Item $resultsDirectory -Recurse -Force +} + +& dotnet test $testProject ` + --configuration $Configuration ` + --settings $settingsFile ` + --collect:"XPlat Code Coverage" ` + --results-directory $resultsDirectory + +if ($LASTEXITCODE -ne 0) { + throw "Coverage test run failed with exit code $LASTEXITCODE." +} + +$reports = @(Get-ChildItem $resultsDirectory -Filter "coverage.cobertura.xml" -Recurse) +if ($reports.Count -ne 1) { + throw "Expected one Cobertura report, but found $($reports.Count)." +} + +[xml]$coverage = Get-Content $reports[0].FullName +$packages = @($coverage.coverage.packages.package) +$unexpectedPackages = @($packages | Where-Object { $_.name -ne "PdfSharpDslCore" }) +if ($packages.Count -ne 1 -or $unexpectedPackages.Count -ne 0) { + $packageNames = ($packages | ForEach-Object { $_.name }) -join ", " + throw "Expected coverage only for PdfSharpDslCore, but found: $packageNames" +} + +$lineRate = [double]::Parse($coverage.coverage."line-rate", [Globalization.CultureInfo]::InvariantCulture) +$branchRate = [double]::Parse($coverage.coverage."branch-rate", [Globalization.CultureInfo]::InvariantCulture) + +$summary = [pscustomobject]@{ + Assembly = $packages[0].name + LineCoverage = "{0:P2}" -f $lineRate + Lines = "$($coverage.coverage.'lines-covered')/$($coverage.coverage.'lines-valid')" + BranchCoverage = "{0:P2}" -f $branchRate + Branches = "$($coverage.coverage.'branches-covered')/$($coverage.coverage.'branches-valid')" + Report = $reports[0].FullName +} +$summary | Format-List + +if ($lineRate -lt $MinimumLineRate) { + throw "Line coverage $($summary.LineCoverage) is below the required $(('{0:P2}' -f $MinimumLineRate))." +} \ No newline at end of file From 9b9bc0a5200058847fe1747a62057ca41e91cfe6 Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Fri, 18 Sep 2026 15:36:58 +0200 Subject: [PATCH 3/9] generate full demo --- PdfSharpDslConsole/Program.cs | 3 +- PdfSharpDslConsole/demo.ipdf | 345 ++++++++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 PdfSharpDslConsole/demo.ipdf diff --git a/PdfSharpDslConsole/Program.cs b/PdfSharpDslConsole/Program.cs index 05730d4..55a1491 100644 --- a/PdfSharpDslConsole/Program.cs +++ b/PdfSharpDslConsole/Program.cs @@ -94,7 +94,8 @@ var parser = new Irony.Parsing.Parser(new PdfGrammar()); //var fileName = "pdfsharp-newpage.ipdf"; //var fileName = "pdfsharp.ipdf"; -var fileName = "sample1.ipdf"; +//var fileName = "sample1.ipdf"; +var fileName = "demo.ipdf"; if (args.Length > 0) { fileName = args[0]; diff --git a/PdfSharpDslConsole/demo.ipdf b/PdfSharpDslConsole/demo.ipdf new file mode 100644 index 0000000..4db5d8b --- /dev/null +++ b/PdfSharpDslConsole/demo.ipdf @@ -0,0 +1,345 @@ +# PdfSharpDsl feature demonstration +# Run from PdfSharpDslConsole with: dotnet run -- demo.ipdf + +# Reusable components. UDFs may be called before or after their definitions. +UDF SECTION(TITLETEXT) + SET FONT Name="Arial" Size=20 bold; + SET BRUSH darkblue; + TITLE Margin=24 HAlign=hcenter Text=$TITLETEXT; + SET PEN steelblue 1; + LINE 36,54,$PAGEWIDTH-36,54; +ENDUDF + +UDF BADGE(X,Y,LABEL) + SET PEN darkblue 1 solid; + SET BRUSH lightblue; + FILLRECT $X,$Y,110,30; + SET FONT Name="Arial" Size=10 bold; + SET BRUSH darkblue; + LINETEXT $X,$Y,110,30 HAlign=hcenter VAlign=vcenter Text=$LABEL; +ENDUDF + +# Special callback: runs after every NEWPAGE and can publish global variables. +UDF __ONNEWPAGE() + SET VAR CONTENTWIDTH=$PAGEWIDTH-72; + SET VAR CONTENTHEIGHT=$PAGEHEIGHT-72; + SET FONT Name="Arial" Size=8 regular; + SET BRUSH gray; + TITLE Margin=-18 HAlign=hcenter Text=("PdfSharpDsl demo | page "+$PAGEINDEX); +ENDUDF + +# ----------------------------------------------------------------------------- +# Cover: pages, page variables, titles, colors, fonts, variables, formulas +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +SET BRUSH darkblue; +SET FONT Name="Arial" Size=28 bold; +TITLE Margin=72 HAlign=hcenter Text="PdfSharpDsl"; +SET FONT Name="Arial" Size=16 italic; +TITLE Margin=112 HAlign=hcenter Text="Complete feature demonstration"; + +SET HBRUSH 0x50ffe9b2; +SET FONT Name="Consolas" Size=10 regular; +TITLE Margin=160 HAlign=hcenter Text=("A4 portrait: "+$PAGEWIDTH+" x "+$PAGEHEIGHT+" points"); +SET HBRUSH 0x00000000; + +SET VAR A=17; +SET VAR B=5; +SET VAR NEGATIVE=-5; +SET VAR POSITIVE=+5; +SET VAR MESSAGE="Variables, arithmetic, comparison and concatenation"; +SET BRUSH black; +LINETEXT 54,220 HAlign=left VAlign=top Text=$MESSAGE; +LINETEXT 54,246 HAlign=left VAlign=top Text=("A+B="+($A+$B)+", A-B="+($A-$B)+", A*B="+($A*$B)); +LINETEXT 54,270 HAlign=left VAlign=top Text=("A/B="+($A/$B)+", A%B="+($A%$B)+", unary values="+$NEGATIVE+" and "+$POSITIVE); + +IF (($A > $B) and ($A >= 17)) THEN + CALL BADGE(54,310,"AND condition: true"); +ELSE + CALL BADGE(54,310,"AND condition: false"); +ENDIF + +IF (($A < $B) or ($A <= 17)) THEN + CALL BADGE(184,310,"OR condition: true"); +ENDIF + +IF ("alpha" <> "beta") THEN + CALL BADGE(314,310,"<> comparison"); +ENDIF + +IF ($A == 17) THEN + CALL BADGE(444,310,"== comparison"); +ENDIF + +SET FONT Name="Arial" Size=11 regular; +TEXT 54,380,487,100 MaxWidth=487 Text="This document is itself the example source. Each page groups related commands and uses comments, optional semicolons, expressions, variables, built-in page variables, host formula functions, control flow and reusable UDFs."; + +# ----------------------------------------------------------------------------- +# Drawing primitives, pen styles, brushes and color formats +# ----------------------------------------------------------------------------- +NEWPAGE A4 landscape; +CALL SECTION("Drawing primitives and styles"); + +SET FONT Name="Consolas" Size=9 regular; +SET BRUSH black; +SET PEN black 1 solid; +LINETEXT 45,90 Text="solid"; +LINE 45,108,165,108; +SET PEN red 1.5 dash; +LINETEXT 45,130 Text="dash"; +LINE 45,148,165,148; +SET PEN green 2 dot; +LINETEXT 45,170 Text="dot"; +LINE 45,188,165,188; +SET PEN blue 2 dashdot; +LINETEXT 45,210 Text="dashdot"; +LINE 45,228,165,228; +SET PEN 0xff8844 2 dashdotdot; +LINETEXT 45,250 Text="dashdotdot / RGB hex"; +LINE 45,268,165,268; + +SET PEN black 1 solid; +SET BRUSH lightgreen; +RECT 220,90,100,65; +FILLRECT 350,90,100,65; +SET BRUSH 0x8090caf9; +ELLIPSE 220,190,100,65; +FILLELLIPSE 350,190,100,65; + +SET BRUSH gold; +PIE 500,90,110,110 Start=15 Angle=120; +SET BRUSH tomato; +FILLPIE 640,90,110,110 Start=210 Angle=120; + +SET PEN darkslategray 2 solid; +POLYGON 500,250,550,210,610,250,585,315,525,315; +SET BRUSH lightseagreen; +FILLPOLYGON 640,250,690,210,750,250,725,315,665,315; + +SET PEN purple 3 solid; +MOVETO 220,350; +LINETO 280,320; +LINETO 340,370; +LINETO 400,330; +LINETO 450,370; + +SET PEN g0.45 1; +SET BRUSH g0.80; +FILLRECT 500,350,250,60; +SET BRUSH black; +LINETEXT 625,380 HAlign=hcenter VAlign=vcenter Text="grayscale colors: g0.45 / g0.80"; + +# Negative dimensions extend to the far page edge. +SET PEN crimson 1 dash; +RECT 20,440,-20,-55; +SET BRUSH crimson; +LINETEXT 30,450 Text="Negative width and height use the remaining page area"; + +# ----------------------------------------------------------------------------- +# Text, alignment, orientation, highlighting and every font style +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Text layout, orientation and fonts"); +SET PEN lightgray 0.5; +RECT 45,85,500,110; +SET FONT Name="Arial" Size=11 regular; +SET BRUSH black; +LINETEXT 45,85,500,110 HAlign=left VAlign=top Text="left / top"; +LINETEXT 45,85,500,110 HAlign=hcenter VAlign=vcenter Text="center / center"; +LINETEXT 45,85,500,110 HAlign=right VAlign=bottom Text="right / bottom"; + +SET HBRUSH 0x60fff176; +LINETEXT 60,235 HAlign=left VAlign=vcenter Orientation=horizontal Text="highlight brush"; +SET HBRUSH 0x00000000; +LINETEXT 220,235 HAlign=left VAlign=vcenter Orientation=vertical Text="vertical"; +LINETEXT 300,235 HAlign=left VAlign=vcenter Orientation=30 Text="30 degree rotation"; + +SET FONT Name="Arial" Size=11 regular; +LINETEXT 45,330 Text="regular"; +SET FONT Name="Arial" Size=11 bold; +LINETEXT 140,330 Text="bold"; +SET FONT Name="Arial" Size=11 italic; +LINETEXT 220,330 Text="italic"; +SET FONT Name="Arial" Size=11 bolditalic; +LINETEXT 300,330 Text="bold italic"; +SET FONT Name="Arial" Size=11 underline; +LINETEXT 405,330 Text="underline"; +SET FONT Name="Arial" Size=11 strikeout; +LINETEXT 500,330 Text="strikeout"; + +SET FONT Name="Arial" Size=11 regular; +TEXT 45,385 MaxWidth=220 Text="TEXT with a point and MaxWidth wraps a longer paragraph over several lines."; +TEXT 300,385,245,95 Text="TEXT with a rectangle supports multiline content.\r\nSecond explicit line.\r\nThird explicit line."; + +TITLE Margin=515 HAlign=left Text="Left title"; +TITLE Margin=515 HAlign=hcenter Text="Centered title"; +TITLE Margin=515 HAlign=right Text="Right title"; +TITLE Margin=-40 HAlign=right Text="Negative title margin is measured from the bottom"; + +# ----------------------------------------------------------------------------- +# FOR loops, UDF parameters/scoping, IF/ELSE and formula-driven geometry +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Control flow and reusable drawing"); +SET FONT Name="Consolas" Size=10 regular; +SET BRUSH black; +LINETEXT 45,85 Text="FOR is inclusive; UDF parameters are locally scoped."; + +FOR ROW=0 TO 6 DO + SET PEN 0x4472c4 1; + LINE 70,130+$ROW*55,520,130+$ROW*55; + CALL BADGE(80,142+$ROW*55,"Iteration "+$ROW); +ENDFOR + +IF (($A >= $B) and ($B <= $A)) THEN + SET BRUSH darkgreen; + LINETEXT 45,555 Text="IF branch selected: A >= B and B <= A"; +ELSE + SET BRUSH red; + LINETEXT 45,555 Text="ELSE branch selected"; +ENDIF + +# ----------------------------------------------------------------------------- +# Ordinary tables: auto/fixed widths, header styles, rows and row heights +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Tables with explicit rows"); +SET FONT Name="Arial" Size=9 regular; +SET PEN slategray 0.6; +TABLE 40,90 +HEAD 0xffe8eef7 +COL Width=auto MaxWidth=130 FONT="Arial",9,bold white steelblue "Feature"; +COL Width=140 MaxWidth=180 FONT="Arial",9,bold white steelblue "Syntax"; +COL Width=auto MaxWidth=240 FONT="Arial",9,bold white steelblue "Notes"; +ENDHEAD +ROW +COL "Automatic sizing"; +COL "Width=auto"; +COL "Desired width follows content up to MaxWidth"; +ENDROW +ROW 48 +COL "Fixed row height"; +COL "ROW 48"; +COL "A formula may define the row height"; +ENDROW +ROW +COL "Multiline cell"; +COL "COL expression"; +COL "Line one\r\nLine two\r\nLine three"; +ENDROW +ROW +COL "Short row"; +COL "Missing cells"; +ENDROW +ENDTABLE + +# Table row templates evaluate every COL expression for each ROWINDEX. +SET BRUSH darkblue; +LINETEXT 40,360 Text="Table ROWTEMPLATE with host functions"; +TABLE 40,385 +HEAD lightgray +COL Width=100 MaxWidth=120 FONT="Consolas",8,bold black lightgray "Date"; +COL Width=130 MaxWidth=160 "Comment count"; +COL Width=auto MaxWidth=250 "Evaluated label"; +ENDHEAD +ROWTEMPLATE getGlobalCommentsCount() +COL getGlobalCommentDate($ROWINDEX); +COL getCommentsCount($ROWINDEX); +COL "Global item "+($ROWINDEX+1); +ENDROW +ENDTABLE + +# ----------------------------------------------------------------------------- +# Free row templates: nesting, names, borders, pagination and measured height +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Paginating and nested row templates"); +SET FONT Name="Arial" Size=9 regular; +SET PEN steelblue 0.7; +SET BRUSH black; + +ROWTEMPLATE Count=3 Y=90 Name="outer" BorderSize=5 NewPageTopMargin=70 + SET VAR OUTERINDEX=$ROWINDEX; + LINETEXT 45,0 Text=("Group "+$OUTERINDEX); + ROWTEMPLATE Count=2 Y=16 Name=("inner-"+$OUTERINDEX) BorderSize=3 + LINETEXT 130,0 Text=("Nested item "+$OUTERINDEX+"."+$ROWINDEX); + LINE 125,14,500,14; + ENDROWTEMPLATE + # LASTTEMPLATEHEIGHT excludes the nested template's Y and initial border. + RECT 35,0,490,16+3+$LASTTEMPLATEHEIGHT; +ENDROWTEMPLATE + +SET BRUSH darkblue; +LINETEXT 45,300 Text=("Measured outer template height: "+$LASTTEMPLATEHEIGHT); + +# This template is tall enough to create another page automatically. +ROWTEMPLATE Count=16 Y=340 Name="paginated-list" BorderSize=4 NewPageTopMargin=70 + SET PEN lightgray 0.5; + RECT 45,0,500,36; + SET BRUSH black; + LINETEXT 58,18 VAlign=vcenter Text=("Paginated item "+($ROWINDEX+1)); +ENDROWTEMPLATE + +# ----------------------------------------------------------------------------- +# Images: file/data sources, natural size, points/pixels, fit/crop +# ----------------------------------------------------------------------------- +NEWPAGE A4 landscape; +CALL SECTION("Images from files and embedded data"); +SET FONT Name="Consolas" Size=9 regular; +SET BRUSH black; +LINETEXT 45,82 Text="Natural size from Source"; +IMAGE 45,105 Source="./imageTest.jpg"; + +LINETEXT 300,82 Text="Point rectangle, fit"; +IMAGE 300,105,180,120 point fit Source="./imageTest.jpg"; + +LINETEXT 520,82 Text="Pixel rectangle, crop"; +IMAGE 520,105,180,120 pixel crop Source="./imageTest.jpg"; + +LINETEXT 45,330 Text="Embedded one-pixel PNG using Data"; +IMAGE 45,355,160,100 point fit Data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; + +# ----------------------------------------------------------------------------- +# Host formula functions and custom bundled fonts +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Host functions and bundled fonts"); +SET VAR FONTCOUNT=GetFontCount(); +SET FONT Name="Consolas" Size=9 regular; +SET BRUSH black; +LINETEXT 45,82 Text=("GetFontCount() returned "+$FONTCOUNT); +FOR I=0 TO $FONTCOUNT-1 DO + SET VAR FONTNAME=GetFont($I); + SET FONT Name=$FONTNAME Size=18 regular; + LINETEXT 55,125+70*$I Text=("PdfSharpDsl - "+$FONTNAME); +ENDFOR + +# ----------------------------------------------------------------------------- +# Debug overlays and custom view coordinates +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +# DEBUGOPTIONS is document-wide because the visitor reads it before drawing page 1. +# Uncomment this line to enable every overlay on every page: +# DEBUGOPTIONS DEBUG_TEXT, DEBUG_RECT, DEBUG_ROWTEMPLATE, DEBUG_IMAGE, DEBUG_RULE, DEBUG_ALL; +CALL SECTION("Debug options (opt-in, document-wide)"); +SET FONT Name="Arial" Size=11 regular; +SET BRUSH black; +LINETEXT 60,110,470,50 HAlign=hcenter VAlign=vcenter Text="DEBUGOPTIONS can expose text, rectangles, templates, images and rules across the document."; +SET BRUSH lightsalmon; +FILLRECT 100,200,150,80; +IMAGE 330,200,150,80 point fit Data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; +ROWTEMPLATE Count=2 Y=330 Name="debug-template" BorderSize=3 + LINETEXT 100,0 Text=("Debug row "+$ROWINDEX); + LINE 100,24,480,24; +ENDROWTEMPLATE + +# VIEWSIZE maps a compact coordinate system onto the page. +NEWPAGE A4 portrait; +VIEWSIZE 100,140; +SET PEN red 0.2 dash; +SET BRUSH black; +SET FONT Name="Consolas" Size=3 regular; +LINE 50,0,50,140; +LINE 0,70,100,70; +RECT 2,2,96,136; +LINETEXT 50,70 HAlign=hcenter VAlign=vcenter Text="VIEWSIZE 100 x 140"; \ No newline at end of file From 11507594221b53fcc8aeecd95a8853d3e1157c1a Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Sun, 20 Sep 2026 09:08:57 +0200 Subject: [PATCH 4/9] add more feature in demo --- PdfSharpDslConsole/demo.ipdf | 426 ++++++++++++++++++++++++++++++++++- 1 file changed, 425 insertions(+), 1 deletion(-) diff --git a/PdfSharpDslConsole/demo.ipdf b/PdfSharpDslConsole/demo.ipdf index 4db5d8b..09c06e9 100644 --- a/PdfSharpDslConsole/demo.ipdf +++ b/PdfSharpDslConsole/demo.ipdf @@ -135,6 +135,264 @@ RECT 20,440,-20,-55; SET BRUSH crimson; LINETEXT 30,450 Text="Negative width and height use the remaining page area"; +# ----------------------------------------------------------------------------- +# Page 3: concrete charts built from the drawing primitives +# ----------------------------------------------------------------------------- +NEWPAGE A4 landscape; +CALL SECTION("Concrete charts from drawing primitives"); +SET FONT Name="Arial" Size=9 regular; +SET BRUSH black; + +# Bar chart: monthly sales in thousands. +LINETEXT 45,82 Text="Monthly sales (kEUR)"; +SET PEN lightgray 0.5 dash; +LINE 70,145,390,145; +LINE 70,195,390,195; +LINE 70,245,390,245; +LINE 70,295,390,295; +SET PEN darkslategray 1 solid; +LINE 70,320,390,320; +LINE 70,120,70,320; +SET BRUSH steelblue; +FILLRECT 92,235,32,85; +FILLRECT 142,200,32,120; +FILLRECT 192,220,32,100; +FILLRECT 242,160,32,160; +FILLRECT 292,180,32,140; +FILLRECT 342,135,32,185; +SET BRUSH black; +LINETEXT 96,330 Text="Jan"; +LINETEXT 146,330 Text="Feb"; +LINETEXT 196,330 Text="Mar"; +LINETEXT 246,330 Text="Apr"; +LINETEXT 296,330 Text="May"; +LINETEXT 346,330 Text="Jun"; +LINETEXT 52,320 HAlign=right Text="0"; +LINETEXT 52,270 HAlign=right Text="20"; +LINETEXT 52,220 HAlign=right Text="40"; +LINETEXT 52,170 HAlign=right Text="60"; +LINETEXT 52,120 HAlign=right Text="80"; +SET FONT Name="Arial" Size=8 regular; +SET BRUSH darkslategray; +LINETEXT 92,225 Text="34"; +LINETEXT 142,190 Text="48"; +LINETEXT 192,210 Text="40"; +LINETEXT 242,150 Text="64"; +LINETEXT 292,170 Text="56"; +LINETEXT 342,125 Text="74"; + +# Line chart: support response time, lower is better. +SET FONT Name="Arial" Size=9 regular; +SET BRUSH black; +LINETEXT 440,82 Text="Average response time (ms)"; +SET PEN lightgray 0.5 dash; +LINE 470,145,755,145; +LINE 470,195,755,195; +LINE 470,245,755,245; +LINE 470,295,755,295; +SET PEN darkslategray 1 solid; +LINE 470,320,755,320; +LINE 470,120,470,320; +SET PEN tomato 2 solid; +LINE 490,170,540,210; +LINE 540,210,590,185; +LINE 590,185,640,250; +LINE 640,250,690,230; +LINE 690,230,740,285; +SET BRUSH tomato; +FILLELLIPSE 485,165,10,10; +FILLELLIPSE 535,205,10,10; +FILLELLIPSE 585,180,10,10; +FILLELLIPSE 635,245,10,10; +FILLELLIPSE 685,225,10,10; +FILLELLIPSE 735,280,10,10; +SET BRUSH black; +LINETEXT 485,330 Text="Jan"; +LINETEXT 535,330 Text="Feb"; +LINETEXT 585,330 Text="Mar"; +LINETEXT 635,330 Text="Apr"; +LINETEXT 685,330 Text="May"; +LINETEXT 735,330 Text="Jun"; +LINETEXT 452,320 HAlign=right Text="0"; +LINETEXT 452,270 HAlign=right Text="100"; +LINETEXT 452,220 HAlign=right Text="200"; +LINETEXT 452,170 HAlign=right Text="300"; +LINETEXT 452,120 HAlign=right Text="400"; +SET FONT Name="Arial" Size=8 regular; +SET BRUSH tomato; +LINETEXT 480,155 Text="300"; +LINETEXT 530,195 Text="220"; +LINETEXT 580,170 Text="270"; +LINETEXT 630,235 Text="140"; +LINETEXT 680,215 Text="180"; +LINETEXT 730,270 Text="70"; + +# Additional series demonstrate the available dash styles. +SET PEN steelblue 1.5 dash; +LINE 490,190,540,160; +LINE 540,160,590,220; +LINE 590,220,640,205; +LINE 640,205,690,260; +LINE 690,260,740,240; +SET PEN darkgreen 1.5 dot; +LINE 490,230,540,250; +LINE 540,250,590,235; +LINE 590,235,640,270; +LINE 640,270,690,255; +LINE 690,255,740,295; +SET PEN purple 1.5 dashdot; +LINE 490,150,540,180; +LINE 540,180,590,165; +LINE 590,165,640,195; +LINE 640,195,690,175; +LINE 690,175,740,215; +SET BRUSH black; +LINETEXT 470,355 Text="API p95"; +LINETEXT 555,355 Text="API p50"; +LINETEXT 640,355 Text="Cache"; +SET PEN tomato 3 solid; +LINE 515,352,535,352; +SET PEN steelblue 3 dash; +LINE 600,352,620,352; +SET PEN darkgreen 3 dot; +LINE 680,352,700,352; +SET PEN purple 3 dashdot; +LINE 750,352,770,352; +LINETEXT 775,355 Text="DB"; + +# Pie chart: traffic sources, using filled sectors. +SET PEN black 1; +SET FONT Name="Arial" Size=9 regular; +SET BRUSH black; +LINETEXT 45,390 Text="Traffic sources"; +SET BRUSH steelblue; +FILLPIE 130,410,130,130 Start=0 Angle=180; +SET BRUSH lightgreen; +FILLPIE 130,410,130,130 Start=180 Angle=108; +SET BRUSH gold; +FILLPIE 130,410,130,130 Start=288 Angle=72; +SET BRUSH black; +LINETEXT 285,435 Text="Direct 50%"; +LINETEXT 285,465 Text="Search 30%"; +LINETEXT 285,495 Text="Referral 20%"; +SET PEN steelblue 8 solid; +LINE 270,430,280,430; +SET PEN lightgreen 8 solid; +LINE 270,460,280,460; +SET PEN gold 8 solid; +LINE 270,490,280,490; + +# ----------------------------------------------------------------------------- +# Page 4: a compact business report combining metrics, chart and table +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Monthly business report"); +SET VAR REVENUE=1284; +SET VAR ORDERS=342; +SET VAR SATISFACTION=94; +SET FONT Name="Arial" Size=9 regular; +SET BRUSH black; + +# KPI cards. +SET BRUSH 0xffe8f1fb; +FILLRECT 45,85,155,68; +SET BRUSH 0xffe8f7ee; +FILLRECT 220,85,155,68; +SET BRUSH 0xfffff3d6; +FILLRECT 395,85,155,68; +SET FONT Name="Arial" Size=9 bold; +SET BRUSH darkslategray; +LINETEXT 60,101 Text="REVENUE"; +LINETEXT 235,101 Text="ORDERS"; +LINETEXT 410,101 Text="CUSTOMER SATISFACTION"; +SET FONT Name="Arial" Size=20 bold; +SET BRUSH darkblue; +LINETEXT 60,118 Text=($REVENUE+" kEUR"); +SET BRUSH darkgreen; +LINETEXT 235,118 Text=$ORDERS; +SET BRUSH orange; +LINETEXT 410,118 Text=($SATISFACTION+"%"); + +# Revenue trend. +SET FONT Name="Arial" Size=11 bold; +SET BRUSH black; +LINETEXT 45,190 Text="Revenue trend (kEUR)"; +SET PEN lightgray 0.5 dash; +LINE 70,250,525,250; +LINE 70,300,525,300; +LINE 70,350,525,350; +LINE 70,400,525,400; +SET PEN darkslategray 1 solid; +LINE 70,425,525,425; +LINE 70,225,70,425; +SET PEN steelblue 2.5 solid; +LINE 90,375,160,340; +LINE 160,340,230,355; +LINE 230,355,300,305; +LINE 300,305,370,285; +LINE 370,285,440,250; +LINE 440,250,510,235; +SET BRUSH steelblue; +FILLELLIPSE 85,370,10,10; +FILLELLIPSE 155,335,10,10; +FILLELLIPSE 225,350,10,10; +FILLELLIPSE 295,300,10,10; +FILLELLIPSE 365,280,10,10; +FILLELLIPSE 435,245,10,10; +FILLELLIPSE 505,230,10,10; +SET FONT Name="Arial" Size=8 regular; +SET BRUSH black; +LINETEXT 82,440 Text="Jan"; +LINETEXT 152,440 Text="Feb"; +LINETEXT 222,440 Text="Mar"; +LINETEXT 292,440 Text="Apr"; +LINETEXT 362,440 Text="May"; +LINETEXT 432,440 Text="Jun"; +LINETEXT 52,425 HAlign=right Text="0"; +LINETEXT 52,375 HAlign=right Text="250"; +LINETEXT 52,325 HAlign=right Text="500"; +LINETEXT 52,275 HAlign=right Text="750"; +LINETEXT 52,225 HAlign=right Text="1000"; + +# Summary table. +SET FONT Name="Arial" Size=11 bold; +SET BRUSH black; +LINETEXT 45,490 Text="Channel summary"; +SET PEN steelblue 1; +SET BRUSH 0xffdce8f5; +FILLRECT 45,510,505,30; +SET BRUSH black; +LINETEXT 58,520 Text="Channel"; +LINETEXT 230,520 Text="Revenue"; +LINETEXT 335,520 Text="Target"; +LINETEXT 440,520 Text="Variance"; +SET BRUSH white; +FILLRECT 45,540,505,30; +FILLRECT 45,600,505,30; +SET BRUSH black; +LINETEXT 58,550 Text="Online"; +LINETEXT 230,550 Text="620 kEUR"; +LINETEXT 335,550 Text="580 kEUR"; +LINETEXT 440,550 Text="+40"; +LINETEXT 58,580 Text="Retail"; +LINETEXT 230,580 Text="410 kEUR"; +LINETEXT 335,580 Text="450 kEUR"; +LINETEXT 440,580 Text="-40"; +LINETEXT 58,610 Text="Partners"; +LINETEXT 230,610 Text="254 kEUR"; +LINETEXT 335,610 Text="220 kEUR"; +LINETEXT 440,610 Text="+34"; +SET PEN lightgray 0.5; +LINE 45,540,550,540; +LINE 45,570,550,570; +LINE 45,600,550,600; +LINE 45,630,550,630; +LINE 215,510,215,630; +LINE 320,510,320,630; +LINE 425,510,425,630; +SET BRUSH darkgreen; +LINETEXT 45,675 Text="Positive variance indicates performance above target."; + # ----------------------------------------------------------------------------- # Text, alignment, orientation, highlighting and every font style # ----------------------------------------------------------------------------- @@ -249,6 +507,49 @@ COL "Global item "+($ROWINDEX+1); ENDROW ENDTABLE +# ----------------------------------------------------------------------------- +# Merged table cells, per-cell alignment and multiline content +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Merged cells and cell alignment"); +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 + +SET BRUSH darkslategray; +LINETEXT 45,400 Text="ColSpan and RowSpan reserve covered cells; no placeholder COL is required."; +LINETEXT 45,425 Text="HAlign accepts left, hcenter or right; VAlign accepts top, vcenter or bottom."; + # ----------------------------------------------------------------------------- # Free row templates: nesting, names, borders, pagination and measured height # ----------------------------------------------------------------------------- @@ -287,18 +588,26 @@ NEWPAGE A4 landscape; CALL SECTION("Images from files and embedded data"); SET FONT Name="Consolas" Size=9 regular; SET BRUSH black; +# Images use 96 DPI: one pixel is 72/96 points. +SET VAR PIXEL_TO_POINT=72/96; LINETEXT 45,82 Text="Natural size from Source"; IMAGE 45,105 Source="./imageTest.jpg"; +SET PEN lightgray 1; +RECT 45,105,200*$PIXEL_TO_POINT,200*$PIXEL_TO_POINT; + LINETEXT 300,82 Text="Point rectangle, fit"; IMAGE 300,105,180,120 point fit Source="./imageTest.jpg"; +RECT 300,105,180,120; LINETEXT 520,82 Text="Pixel rectangle, crop"; IMAGE 520,105,180,120 pixel crop Source="./imageTest.jpg"; +RECT 520,105,180*$PIXEL_TO_POINT,120*$PIXEL_TO_POINT; LINETEXT 45,330 Text="Embedded one-pixel PNG using Data"; IMAGE 45,355,160,100 point fit Data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; + # ----------------------------------------------------------------------------- # Host formula functions and custom bundled fonts # ----------------------------------------------------------------------------- @@ -342,4 +651,119 @@ SET FONT Name="Consolas" Size=3 regular; LINE 50,0,50,140; LINE 0,70,100,70; RECT 2,2,96,136; -LINETEXT 50,70 HAlign=hcenter VAlign=vcenter Text="VIEWSIZE 100 x 140"; \ No newline at end of file +LINETEXT 50,70 HAlign=hcenter VAlign=vcenter Text="VIEWSIZE 100 x 140"; + +# ----------------------------------------------------------------------------- +# Page 13: advanced typography and annotations +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Advanced typography and annotations"); +SET FONT Name="Arial" Size=12 regular; +SET BRUSH black; +LINETEXT 45,90 Text="Text can be aligned, rotated, highlighted and wrapped."; +SET PEN lightgray 0.7 solid; +RECT 45,125,505,105; +SET HBRUSH 0x60fff176; +SET FONT Name="Arial" Size=11 bold; +LINETEXT 60,140,215,70 HAlign=left VAlign=vcenter Text="Left aligned\r\nwith highlight"; +SET HBRUSH 0x00000000; +SET FONT Name="Arial" Size=11 italic; +LINETEXT 275,140,215,70 HAlign=hcenter VAlign=vcenter Text="Centered\r\nitalic text"; +SET FONT Name="Arial" Size=10 regular; +LINETEXT 60,270,490,70 Text="A bounded text area wraps automatically. This is useful for paragraphs, notes, explanations and generated report content without manually calculating every line."; +SET FONT Name="Arial" Size=10 underline; +LINETEXT 60,390 Text="Underline annotation"; +SET FONT Name="Arial" Size=10 strikeout; +LINETEXT 230,390 Text="Strikeout annotation"; +SET FONT Name="Arial" Size=10 regular; +LINETEXT 100,500 Orientation=30 Text="30 degrees"; +LINETEXT 300,500 Orientation=vertical Text="Vertical label"; +SET BRUSH 0xffe8eef7; +FILLRECT 45,570,505,95; +SET BRUSH darkblue; +SET FONT Name="Arial" Size=11 bold; +LINETEXT 65,590 Text="Annotation block"; +SET FONT Name="Arial" Size=10 regular; +SET BRUSH black; +TEXT 65,615,465,35 Text="Use TEXT for a bounded paragraph and LINETEXT for precise alignment."; + +# ----------------------------------------------------------------------------- +# Page 14: shapes, transparency and visual composition +# ----------------------------------------------------------------------------- +NEWPAGE A4 landscape; +CALL SECTION("Shapes, transparency and composition"); +SET FONT Name="Arial" Size=10 regular; +SET BRUSH black; +LINETEXT 45,85 Text="Layered shapes can create panels, badges and visual emphasis."; +SET PEN steelblue 1.5 solid; +SET BRUSH 0x405f9ed1; +FILLRECT 60,120,260,180; +SET BRUSH 0x5094d36b; +FILLRECT 180,180,260,180; +SET BRUSH 0x50f5b642; +FILLRECT 300,120,260,180; +SET BRUSH black; +LINETEXT 110,205 Text="panel A"; +LINETEXT 230,265 Text="panel B"; +LINETEXT 430,205 Text="panel C"; +SET PEN darkslategray 2 solid; +SET BRUSH 0x80ffffff; +FILLPOLYGON 610,130,700,110,770,180,735,280,640,300,585,220; +SET BRUSH black; +LINETEXT 625,205 Text="polygon"; +SET PEN purple 3 dashdot; +SET BRUSH 0x60ff7f50; +FILLPIE 90,390,130,130 Start=20 Angle=220; +SET BRUSH 0x6050c878; +FILLPIE 270,390,130,130 Start=140 Angle=220; +SET BRUSH 0x60f5b642; +FILLPIE 450,390,130,130 Start=260 Angle=220; +SET BRUSH black; +LINETEXT 95,535 Text="transparency"; +LINETEXT 275,535 Text="overlapping arcs"; +LINETEXT 455,535 Text="alpha colors"; + +# ----------------------------------------------------------------------------- +# Page 15: reusable layout patterns +# ----------------------------------------------------------------------------- +NEWPAGE A4 portrait; +CALL SECTION("Reusable layout patterns"); +SET FONT Name="Arial" Size=10 regular; +SET BRUSH black; +SET PEN lightgray 0.8 solid; +RECT 45,85,505,600; +SET BRUSH 0xffe8eef7; +FILLRECT 45,85,505,70; +SET BRUSH darkblue; +SET FONT Name="Arial" Size=16 bold; +LINETEXT 65,105 Text="Document header"; +SET FONT Name="Arial" Size=9 regular; +LINETEXT 65,133 Text="A reusable page frame built from rectangles, text and lines."; +SET BRUSH 0xfff5f8fb; +FILLRECT 65,185,225,150; +FILLRECT 305,185,225,150; +SET BRUSH black; +SET FONT Name="Arial" Size=11 bold; +LINETEXT 85,205 Text="Left column"; +LINETEXT 325,205 Text="Right column"; +SET FONT Name="Arial" Size=10 regular; +LINETEXT 85,240 Text="Use columns for summaries"; +LINETEXT 85,265 Text="and explanatory notes."; +LINETEXT 325,240 Text="Keep repeated content"; +LINETEXT 325,265 Text="inside a UDF component."; +SET PEN steelblue 1 solid; +LINE 65,365,530,365; +SET BRUSH black; +SET FONT Name="Arial" Size=12 bold; +LINETEXT 65,390 Text="Content section"; +SET FONT Name="Arial" Size=10 regular; +TEXT 65,425,465,90 MaxWidth=465 Text="A page can combine a fixed frame, reusable UDFs, measured row templates and automatic pagination. This pattern is suitable for invoices, reports and generated documentation."; +SET PEN steelblue 0.8 solid; +LINE 45,715,550,715; +SET BRUSH 0xffe8f7ee; +FILLRECT 65,730,465,55; +SET BRUSH darkgreen; +SET FONT Name="Arial" Size=11 bold; +LINETEXT 85,740 Text="Footer status"; +SET FONT Name="Arial" Size=10 regular; +LINETEXT 85,765 Text="Generated successfully | ready for review"; \ No newline at end of file From 2de10d085ed8cce9bf24d93c53878d752297dbb2 Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Sun, 20 Sep 2026 09:40:06 +0200 Subject: [PATCH 5/9] fix negative colmaxwidth on linux --- PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs | 2 +- PdfSharpDslCore/Drawing/TableDefinition.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs b/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs index ee69874..d3c2f1d 100644 --- a/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs +++ b/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs @@ -450,7 +450,7 @@ public void DrawTable(double x, double y, TableDefinition tblDef) } if (!testSize) continue; - var w = Math.Min(column.DesiredWidth ?? 0, pageSpaceLeft); + var w = Math.Max(0, Math.Min(column.DesiredWidth ?? 0, pageSpaceLeft)); var measure = sizeFormatter.CalculateTextSize(row.Data[i], xFonts[i], defaultBrush, w); if (rowMeasure) { diff --git a/PdfSharpDslCore/Drawing/TableDefinition.cs b/PdfSharpDslCore/Drawing/TableDefinition.cs index bac79b5..9058ae4 100644 --- a/PdfSharpDslCore/Drawing/TableDefinition.cs +++ b/PdfSharpDslCore/Drawing/TableDefinition.cs @@ -33,7 +33,7 @@ public double ColWidth(int i) /// public double ColMaxWidth(int i, double pageWidth) { - return Math.Min(Columns[i].MaxWidth ?? pageWidth, pageWidth); + return Math.Max(0, Math.Min(Columns[i].MaxWidth ?? pageWidth, pageWidth)); } public XStringAlignment Alignment(int i) From 9069d9bc07f12190d2e1ef5fbab184cf35f55b4c Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Sun, 20 Sep 2026 10:11:18 +0200 Subject: [PATCH 6/9] add merged table cells --- PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs | 196 ++++++++++++++---- PdfSharpDslCore/Drawing/TableDefinition.cs | 11 + PdfSharpDslCore/Parser/PdfDrawerVisitor.cs | 40 +++- PdfSharpDslCore/Parser/PdfGrammar.cs | 6 +- pdfsharpdslTests/GenerationTableTests.cs | 1 + .../ValidInputFiles/pdf1-table-merged.txt | 35 ++++ pdfsharpdslTests/VisitorTests.cs | 44 ++++ pdfsharpdslTests/pdfsharpdslTests.csproj | 3 + 8 files changed, 287 insertions(+), 49 deletions(-) create mode 100644 pdfsharpdslTests/ValidInputFiles/pdf1-table-merged.txt diff --git a/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs b/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs index d3c2f1d..84089be 100644 --- a/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs +++ b/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs @@ -1,6 +1,7 @@ using PdfSharpCore; using PdfSharpCore.Drawing; using PdfSharpCore.Drawing.Layout; +using PdfSharpCore.Drawing.Layout.enums; using PdfSharpCore.Pdf; using SixLabors.ImageSharp; using System; @@ -414,22 +415,29 @@ public void DrawTable(double x, double y, TableDefinition tblDef) i++; } + var placements = LayoutTableCells(tblDef); + //measure all rows var sizeFormatter = new XTextSegmentFormatter(Gfx) { Alignment = XParagraphAlignment.Left }; - foreach (var row in tblDef.Rows) + for (var rowIndex = 0; rowIndex < tblDef.Rows.Count; rowIndex++) { + var row = tblDef.Rows[rowIndex]; var rowMeasure = row.DesiredHeight is null; - for (i = 0; i < row.Data.Length; i++) + foreach (var placement in placements) { - var testSize = !colMeasure[i]; - var pageSpaceLeft = tblDef.ColMaxWidth(i, availableWidth); - var column = tblDef.Columns[i]; - if (colMeasure[i]) + //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(row.Data[i], xFonts[i]); + var cSize = Gfx.MeasureString(text, xFonts[colIndex]); cSize.Width += (margins.Left + margins.Right); if (cSize.Width > pageSpaceLeft) { @@ -442,7 +450,7 @@ public void DrawTable(double x, double y, TableDefinition tblDef) column.DesiredWidth = Math.Max(column.DesiredWidth ?? 0, cSize.Width); } - if (rowMeasure) + if (rowMeasure && placement.RowSpan == 1) { row.DesiredHeight = Math.Max(row.DesiredHeight ?? 0, cSize.Height + margins.Top + margins.Bottom); @@ -451,8 +459,8 @@ public void DrawTable(double x, double y, TableDefinition tblDef) if (!testSize) continue; var w = Math.Max(0, Math.Min(column.DesiredWidth ?? 0, pageSpaceLeft)); - var measure = sizeFormatter.CalculateTextSize(row.Data[i], xFonts[i], defaultBrush, w); - if (rowMeasure) + var measure = sizeFormatter.CalculateTextSize(text, xFonts[colIndex], defaultBrush, w); + if (rowMeasure && placement.RowSpan == 1) { row.DesiredHeight = Math.Max(row.DesiredHeight ?? 0, measure.Height + margins.Top + margins.Bottom); @@ -460,6 +468,30 @@ public void DrawTable(double x, double y, TableDefinition tblDef) } } + //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 = sizeFormatter.CalculateTextSize(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; + } + } + //draw header double offsetX = 0; double offsetY = 0; @@ -494,40 +526,30 @@ public void DrawTable(double x, double y, TableDefinition tblDef) } offsetY = tblDef.HeaderHeight ?? 0; - //draw body - foreach (var row in tblDef.Rows) + //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++) { - if (y + offsetY + row.DesiredHeight > CurrentPage.Height) + 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) { - Gfx.Restore(); - NewPage(); - Gfx.Save(); - //TODO: set top margin - y = 1; - offsetY = 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; + } } - offsetX = 0; - for (i = 0; i < row.Data.Length; i++) + foreach (var placement in rowPlacements) { - var w = tblDef.Columns[i].DrawWidth; - var h = row.DesiredHeight ?? 0; - var r = new XRect(offsetX + x, offsetY + y, w, h); - ResetClip(); - Gfx.DrawRectangle(CurrentPen, tblDef.Columns[i].BackColor, r); - 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.IntersectClip(rText); - var fmt = new XStringFormat - { Alignment = XStringAlignment.Center, LineAlignment = XLineAlignment.Center }; - //to debug - //Gfx.DrawRectangle(XPens.Violet, rText); - //TODO: split to draw one string per line - DrawStringMultiline(row.Data[i], xFonts[i], tblDef.Columns[i].Brush ?? defaultBrush, rText, - fmt); - offsetX += w; + DrawTableCell(x, y + offsetY, placement, tblDef, xFonts, defaultBrush, colX); } offsetY += row.DesiredHeight ?? 0; @@ -539,6 +561,86 @@ public void DrawTable(double x, double y, TableDefinition tblDef) } } + private sealed class TableCellPlacement + { + public TableCellPlacement(int row, int column, int columnSpan, int rowSpan, CellDefinition cell) + { + Row = row; + Column = column; + ColumnSpan = columnSpan; + RowSpan = rowSpan; + Cell = cell; + } + + public int Row { get; } + public int Column { get; } + public int ColumnSpan { get; } + public int RowSpan { get; } + public CellDefinition Cell { get; } + } + + private static List LayoutTableCells(TableDefinition table) + { + var result = new List(); + var columnCount = table.Columns.Count; + var occupiedUntilRow = new int[columnCount]; + 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) + 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; + } + } + + return result; + } + + private void DrawTableCell(double x, double y, TableCellPlacement placement, TableDefinition table, + XFont[] fonts, XBrush defaultBrush, double[] colX) + { + 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 + { + Alignment = placement.Cell.HorizontalAlignment ?? column.Alignment, + LineAlignment = placement.Cell.VerticalAlignment ?? XLineAlignment.Near + }; + DrawStringMultiline(placement.Cell.Text, fonts[placement.Column], column.Brush ?? defaultBrush, rText, + fmt); + } + private void ResetClip() { if (_gfx is null) return; @@ -551,7 +653,21 @@ private void ResetClip() private void DrawStringMultiline(string text, XFont xFont, XBrush xBrush, XRect r, XStringFormat fmt) { - var formatter = new XTextFormatter(Gfx); + var formatter = new XTextFormatter(Gfx) + { + Alignment = fmt.Alignment switch + { + XStringAlignment.Center => XParagraphAlignment.Center, + XStringAlignment.Far => XParagraphAlignment.Right, + _ => XParagraphAlignment.Left + }, + VerticalAlignment = fmt.LineAlignment switch + { + XLineAlignment.Center => XVerticalAlignment.Middle, + XLineAlignment.Far => XVerticalAlignment.Bottom, + _ => XVerticalAlignment.Top + } + }; formatter.DrawString(text, xFont, xBrush, r); } diff --git a/PdfSharpDslCore/Drawing/TableDefinition.cs b/PdfSharpDslCore/Drawing/TableDefinition.cs index 9058ae4..b32ab07 100644 --- a/PdfSharpDslCore/Drawing/TableDefinition.cs +++ b/PdfSharpDslCore/Drawing/TableDefinition.cs @@ -77,5 +77,16 @@ public class RowDefinition /// string because there is only draw text /// public string[] Data { get; set; } = Array.Empty(); + + public List Cells { get; set; } = new(); + } + + 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; } } } \ No newline at end of file diff --git a/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs b/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs index 27c7f38..bd70d7f 100644 --- a/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs +++ b/PdfSharpDslCore/Parser/PdfDrawerVisitor.cs @@ -568,11 +568,10 @@ private void GenerateTableRowsTemplate(IEnumerable nodes, TableDe for (var i = 0; i < rowCount; i++) { var rowDef = new RowDefinition(); - var cols = row.ChildNodes("TableCol").SelectMany(x => x.ChildNodes) - .Where(x => x.Term?.Name != "COL").ToArray(); vars.Add("ROWINDEX", i); - var rowData = cols.Select(x => EvaluateForObject(x)?.ToString()!).ToList(); + rowDef.Cells = row.ChildNodes("TableCol").Select(ParseTableCell).ToList(); + var rowData = rowDef.Cells.Select(cell => cell.Text).ToList(); while (rowData.Count < tbl.Columns.Count) { @@ -604,11 +603,8 @@ private void GenerateTableRows(IEnumerable nodes, TableDefinition rowDef.DesiredHeight = rowHeight; } - var cols = row.ChildNodes("TableCol").SelectMany(x => x.ChildNodes).Where(x => x.Term?.Name != "COL") - .ToArray(); - - - var rowData = cols.Select(x => x.Token.ValueString).ToList(); + rowDef.Cells = row.ChildNodes("TableCol").Select(ParseTableCell).ToList(); + var rowData = rowDef.Cells.Select(cell => cell.Text).ToList(); while (rowData.Count < tbl.Columns.Count) { rowData.Add(string.Empty); @@ -655,6 +651,34 @@ 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) { var hNode = alignNode.Term.Name == "HAlign" ? alignNode : null; diff --git a/PdfSharpDslCore/Parser/PdfGrammar.cs b/PdfSharpDslCore/Parser/PdfGrammar.cs index 6238445..35aeda7 100644 --- a/PdfSharpDslCore/Parser/PdfGrammar.cs +++ b/PdfSharpDslCore/Parser/PdfGrammar.cs @@ -113,6 +113,8 @@ 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"); @@ -333,7 +335,9 @@ public PdfGrammar() TableColWidth.Rule = Arg("Width") + NumberOrAuto + Arg("MaxWidth") + NumberOrAuto; TableColList.Rule = MakeStarRule(TableColList, TableCol); TableRow.Rule = ToTerm("ROW") + TableRowStyle + TableColList + ToTerm("ENDROW"); - TableCol.Rule = ToTerm("COL") + FormulaExpression + semi; + 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"; diff --git a/pdfsharpdslTests/GenerationTableTests.cs b/pdfsharpdslTests/GenerationTableTests.cs index e10f0c4..4bd2112 100644 --- a/pdfsharpdslTests/GenerationTableTests.cs +++ b/pdfsharpdslTests/GenerationTableTests.cs @@ -14,6 +14,7 @@ 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}"); diff --git a/pdfsharpdslTests/ValidInputFiles/pdf1-table-merged.txt b/pdfsharpdslTests/ValidInputFiles/pdf1-table-merged.txt new file mode 100644 index 0000000..3ca9744 --- /dev/null +++ b/pdfsharpdslTests/ValidInputFiles/pdf1-table-merged.txt @@ -0,0 +1,35 @@ +# 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 1243142..bce8941 100644 --- a/pdfsharpdslTests/VisitorTests.cs +++ b/pdfsharpdslTests/VisitorTests.cs @@ -194,6 +194,50 @@ public void TableRowTemplateBuildsRowsWidthsAndPadding() Assert.False(visitor.Vars.ContainsKey("ROWINDEX")); } + [Fact] + public void TableColParsesColSpanRowSpanAndAlignment() + { + var tree = ParseText( + "TABLE 20,30 " + + "HEAD " + + "COL Width=40 MaxWidth=30 \"A\"; " + + "COL Width=auto MaxWidth=100 \"B\"; " + + "ENDHEAD " + + "ROW " + + "COL ColSpan=2 HAlign=hcenter VAlign=vcenter \"merged\"; " + + "ENDROW " + + "ROW " + + "COL RowSpan=2 \"plain\"; " + + "COL \"formula: \"+(1+1); " + + "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); + + Assert.NotNull(capturedTable); + var mergedCell = capturedTable.Rows[0].Cells[0]; + Assert.Equal("merged", mergedCell.Text); + 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); + } + [Fact] public void CustomUdfCanFallBackToDslBodyOrOverrideIt() { diff --git a/pdfsharpdslTests/pdfsharpdslTests.csproj b/pdfsharpdslTests/pdfsharpdslTests.csproj index c721efb..ba69c1c 100644 --- a/pdfsharpdslTests/pdfsharpdslTests.csproj +++ b/pdfsharpdslTests/pdfsharpdslTests.csproj @@ -60,6 +60,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest From 83aa9a57657addbecbc6e8527cd77401b7a5244c Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Sun, 20 Sep 2026 10:23:59 +0200 Subject: [PATCH 7/9] update ci with new action version --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3ddfb3..2f3ebc3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,9 +15,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.0.x - name: Restore dependencies From d398ca2d8342b4d66feb684bb757a68753ecc6f0 Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Sun, 20 Sep 2026 10:26:10 +0200 Subject: [PATCH 8/9] fix table height crash on Linux CI font fallback CalculateTextSize's own wrap-height computation can go negative when the resolved font's metrics are degenerate (no Arial on Linux CI, no cross-platform font resolver registered). Width was already clamped; this wraps the two call sites and falls back to the unwrapped MeasureString, which does not hit that code path. Co-Authored-By: Claude Sonnet 5 --- PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs b/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs index 84089be..1bc855c 100644 --- a/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs +++ b/PdfSharpDslCore/Drawing/PdfDocumentDrawer.cs @@ -459,7 +459,7 @@ public void DrawTable(double x, double y, TableDefinition tblDef) if (!testSize) continue; var w = Math.Max(0, Math.Min(column.DesiredWidth ?? 0, pageSpaceLeft)); - var measure = sizeFormatter.CalculateTextSize(text, xFonts[colIndex], defaultBrush, w); + var measure = SafeCalculateTextSize(sizeFormatter, text, xFonts[colIndex], defaultBrush, w); if (rowMeasure && placement.RowSpan == 1) { row.DesiredHeight = Math.Max(row.DesiredHeight ?? 0, @@ -480,7 +480,7 @@ public void DrawTable(double x, double y, TableDefinition tblDef) 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 = sizeFormatter.CalculateTextSize(placement.Cell.Text, xFonts[placement.Column], + 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(); @@ -641,6 +641,24 @@ private void DrawTableCell(double x, double y, TableCellPlacement placement, Tab 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); + } + } + private void ResetClip() { if (_gfx is null) return; From 9b3206be0a831ee2c358aa75af5e5a30ddeeb73e Mon Sep 17 00:00:00 2001 From: Pierrick Gourlain Date: Sun, 20 Sep 2026 10:32:51 +0200 Subject: [PATCH 9/9] version 1.0.6 --- .github/workflows/release-package.yml | 4 ++-- CHANGELOG.md | 4 ++-- PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj | 4 ++-- PdfSharpDslCore/PdfSharpDslCore.csproj | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 94a0a23..1cf16db 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -11,9 +11,9 @@ jobs: packages: write contents: read steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.0.x diff --git a/CHANGELOG.md b/CHANGELOG.md index 08713a6..6a2c8ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Change log -## Unreleased +## Version 1.0.6 (September 20, 2024) * 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. -* Updated dependencies and resolved known ImageSharp and runtime package advisories. + ## Version 1.0.5 (March 3, 2024) * Update nugets packages and upgrade to .Net 8 diff --git a/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj b/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj index f83c090..31c8db4 100644 --- a/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj +++ b/PdfSharpDslCore.Generator/PdfSharpDslCore.Generator.csproj @@ -3,11 +3,11 @@ netstandard2.0 true - 8.0 + 9.0 Pdf Generation using source generation - 1.0.2 + 1.0.6 Pierrick Gourlain https://github.com/pgourlain/bnf_and_pdf diff --git a/PdfSharpDslCore/PdfSharpDslCore.csproj b/PdfSharpDslCore/PdfSharpDslCore.csproj index 354a1cc..a1e2aa1 100644 --- a/PdfSharpDslCore/PdfSharpDslCore.csproj +++ b/PdfSharpDslCore/PdfSharpDslCore.csproj @@ -7,7 +7,7 @@ enable Generate PDF using DSL PdfSharpDslCore - 1.0.5 + 1.0.6 Pierrick Gourlain A DSL using PdfSharpCore to generate PDF