diff --git a/.github/workflows/capture-benchmark-baseline.yml b/.github/workflows/capture-benchmark-baseline.yml index 49c172d1..c137a98f 100644 --- a/.github/workflows/capture-benchmark-baseline.yml +++ b/.github/workflows/capture-benchmark-baseline.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" - name: Platform key (must resolve to linux-x64) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab517937..b6d1a0b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" @@ -128,7 +128,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" - name: Run the visual-regression gate (PDFium diff vs committed Chrome references) @@ -147,7 +147,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" - name: Run benchmark gate (vs pinned baseline, +25% tolerance) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5e239a66..cec37f8a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,12 +27,12 @@ jobs: - uses: actions/checkout@v7 - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: csharp build-mode: manual @@ -41,6 +41,6 @@ jobs: run: dotnet build NetPdf.slnx -c Release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:csharp" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index aa3b448f..f7d47af0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -54,7 +54,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" - name: Restore local tools (pinned DocFX) diff --git a/.github/workflows/fuzz-smoke.yml b/.github/workflows/fuzz-smoke.yml index 1d968a8f..9bf9cf33 100644 --- a/.github/workflows/fuzz-smoke.yml +++ b/.github/workflows/fuzz-smoke.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v7 - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" @@ -58,7 +58,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" - name: Restore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5bd20bb5..39982d25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: ref: ${{ github.event.inputs.tag || github.ref }} - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" @@ -123,7 +123,7 @@ jobs: environment: nuget-release steps: - name: Set up .NET 10 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: "10.0.x" diff --git a/CHANGELOG.md b/CHANGELOG.md index f15b02c8..948801ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,17 @@ All notable changes to NetPdf are documented here. The format follows [Keep a Ch ## [Unreleased] -Post-`1.0.2` improvements accumulate here until the next release is cut. +Post-`1.1.0` improvements accumulate here until the next release is cut. + +## [1.1.0] + +A minor dependency refresh release that resolves the current AngleSharp vulnerability warning, consolidates the compatible open Dependabot runtime/test-tooling/CI action updates, and migrates code away from SkiaSharp APIs newly obsolete in SkiaSharp 4.x. + +### Changed +- Updated **AngleSharp** to 1.6.0 to resolve GHSA-pgww-w46g-26qg; **AngleSharp.Css** remains pinned to 1.0.0-beta.144 because stable 1.0.0 regresses background-image parsing/painting. +- Updated bundled native/runtime dependencies to **SkiaSharp** 4.150.1 and **HarfBuzzSharp** 14.2.1.1. +- Updated build and test tooling, including **Microsoft.NET.Test.Sdk** 18.8.1, **xunit.runner.visualstudio** 3.1.5, **coverlet.collector** 10.0.1, **BenchmarkDotNet** 0.15.8, and **Microsoft.SourceLink.GitHub** 10.0.301. +- Updated CI actions to **actions/setup-dotnet** v6 and **github/codeql-action** v4. ## [1.0.2] @@ -72,7 +82,8 @@ The first stable release. `HtmlPdf.Convert(html)` runs the full HTML → CSS → - Single `NetPdf` NuGet package bundling the whole engine; optional `NetPdf.Languages.*` hyphenation add-ons. - Source Link + symbol packages for source-stepping. -[Unreleased]: https://github.com/raroche/NetPdf/compare/v1.0.2...HEAD +[Unreleased]: https://github.com/raroche/NetPdf/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/raroche/NetPdf/compare/v1.0.2...v1.1.0 [1.0.2]: https://github.com/raroche/NetPdf/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/raroche/NetPdf/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/raroche/NetPdf/compare/0.9.0-rc1...v1.0.0 diff --git a/Directory.Build.props b/Directory.Build.props index 657e91f8..0f1c1d1b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,13 +29,9 @@ $(WarningsNotAsErrors);NU5104 @@ -64,10 +60,13 @@ - First stable release. Full changelog: https://github.com/raroche/NetPdf/blob/main/CHANGELOG.md + Release notes for this and every prior version: https://github.com/raroche/NetPdf/blob/main/CHANGELOG.md - 1.0.2 + 1.1.0 @@ -101,7 +100,7 @@ true - 1.0.1 + 1.0.2 5.0.0 diff --git a/Directory.Packages.props b/Directory.Packages.props index dd77b43f..b74d2b62 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,37 +12,41 @@ review before being added here (see docs/clean-room-policy.md). --> - + - - - - - - - - + + + + + + + + - - - - + + + + + macOS/Linux/Win) rasterizes the NetPdf PDF -> RGBA; SkiaSharp only WRITES PDF. PDFtoImage 5.2.1 + declares a SkiaSharp FLOOR of 3.119.2; production intentionally pins 4.150.1 above (PR #355), and + NuGet unifies the graph to that single higher version — so the harness still rasterizes against the + SAME SkiaSharp the shipped package uses. The tradeoff of the gap: PDFtoImage runs against a SkiaSharp + newer than it compiled against, so a removed 3.x API would surface as a runtime MissingMethodException + rather than a build break. The RenderingCorpus suite is what proves that hasn't happened — if it ever + fails after a SkiaSharp major bump, suspect this gap first. --> @@ -62,7 +66,7 @@ - + diff --git a/build/version.json b/build/version.json index 72ab7adb..0bc74893 100644 --- a/build/version.json +++ b/build/version.json @@ -1,8 +1,8 @@ { - "version": "1.0.2", + "version": "1.1.0", "phase": 5, - "phaseDescription": "Packaging & release (v1.0 launch)", + "phaseDescription": "Packaging & release", "publicApiFrozen": true, - "lastUpdated": "2026-07-10", + "lastUpdated": "2026-07-27", "_note": "Informational only. The MSBuild source-of-truth is Directory.Build.props (VersionPrefix + VersionSuffix). When bumping versions, update BOTH files together, then create the git tag. ReleaseVersionParityTests guards that this file, Directory.Build.props, and the CHANGELOG's latest version heading agree. Phase 5 will introduce a small MSBuild task that reads this file directly." } diff --git a/src/NetPdf.Pdf/Images/ImageFilterApplier.cs b/src/NetPdf.Pdf/Images/ImageFilterApplier.cs index 094fa943..1a39dec2 100644 --- a/src/NetPdf.Pdf/Images/ImageFilterApplier.cs +++ b/src/NetPdf.Pdf/Images/ImageFilterApplier.cs @@ -115,7 +115,7 @@ internal static class ImageFilterApplier using (chain) using (var paint = new SKPaint { IsAntialias = true, ImageFilter = chain }) { - canvas.DrawBitmap(src, padL, padT, paint); + canvas.DrawBitmap(src, padL, padT, new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.None), paint); } using var image = surface.Snapshot(); diff --git a/src/NetPdf.Pdf/Images/ImageMaskApplier.cs b/src/NetPdf.Pdf/Images/ImageMaskApplier.cs index 3a9b3ad4..8c3a85cf 100644 --- a/src/NetPdf.Pdf/Images/ImageMaskApplier.cs +++ b/src/NetPdf.Pdf/Images/ImageMaskApplier.cs @@ -42,10 +42,11 @@ internal static class ImageMaskApplier // Draw the base, then multiply its alpha by the mask's alpha (DstIn) — the mask scaled to fill // the base's pixel box. Result alpha = baseAlpha × maskAlpha; result RGB = base RGB. - canvas.DrawBitmap(baseDecoded, 0, 0); + var sampling = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.None); + canvas.DrawBitmap(baseDecoded, 0, 0, sampling); using (var paint = new SKPaint { IsAntialias = true, BlendMode = SKBlendMode.DstIn }) { - canvas.DrawBitmap(maskDecoded, new SKRect(0, 0, w, h), paint); + canvas.DrawBitmap(maskDecoded, new SKRect(0, 0, w, h), sampling, paint); } using var image = surface.Snapshot(); diff --git a/src/NetPdf.Pdf/Images/TextShadowRasterizer.cs b/src/NetPdf.Pdf/Images/TextShadowRasterizer.cs index 52823979..fd6c122e 100644 --- a/src/NetPdf.Pdf/Images/TextShadowRasterizer.cs +++ b/src/NetPdf.Pdf/Images/TextShadowRasterizer.cs @@ -112,17 +112,16 @@ internal static class TextShadowRasterizer { destOffsetXPx = destOffsetYPx = destWidthPx = destHeightPx = 0; - using var fullPath = new SKPath(); + using var fullPathBuilder = new SKPathBuilder(); for (var i = 0; i < glyphArray.Length; i++) { using var glyphPath = font.GetGlyphPath(glyphArray[i]); if (glyphPath is not null && !glyphPath.IsEmpty) { - using var placed = new SKPath(); - glyphPath.Transform(SKMatrix.CreateTranslation(penXDevice[i], 0), placed); - fullPath.AddPath(placed); + fullPathBuilder.AddPath(glyphPath, penXDevice[i], 0, SKPathAddMode.Append); } } + using var fullPath = fullPathBuilder.Detach(); if (fullPath.IsEmpty) return null; // an all-whitespace run paints no shadow. var ink = fullPath.TightBounds; diff --git a/src/NetPdf.Svg/SvgClipMask.cs b/src/NetPdf.Svg/SvgClipMask.cs index 32bb8e40..1994338c 100644 --- a/src/NetPdf.Svg/SvgClipMask.cs +++ b/src/NetPdf.Svg/SvgClipMask.cs @@ -52,7 +52,8 @@ public static void ApplyClipPath(SKCanvas canvas, XElement el, SvgStyle style, S /// when no child contributes geometry. private static SKPath? BuildClipPathGeometry(XElement clip, SvgStyle style, SvgRenderState state) { - SKPath? acc = null; + using var acc = new SKPathBuilder(); + var hasPath = false; foreach (var child in clip.Elements()) { var target = child; @@ -71,10 +72,10 @@ public static void ApplyClipPath(SKCanvas canvas, XElement el, SvgStyle style, S if (SvgTransform.Parse(SvgRasterizer.Attr(target, "transform")) is { } tm) sp.Transform(tm); if (useOffset is { } uo) sp.Transform(uo); if (useTransform is { } ut2) sp.Transform(ut2); - if (acc is null) acc = new SKPath(sp); - else acc.AddPath(sp); + acc.AddPath(sp, SKPathAddMode.Append); + hasPath = true; } - return acc; + return hasPath ? acc.Detach() : null; } /// The geometry bounding box of an element in its OWN coordinate space (its own diff --git a/src/NetPdf.Svg/SvgRasterizer.cs b/src/NetPdf.Svg/SvgRasterizer.cs index 8d1495c9..f648344a 100644 --- a/src/NetPdf.Svg/SvgRasterizer.cs +++ b/src/NetPdf.Svg/SvgRasterizer.cs @@ -316,45 +316,48 @@ internal static void RenderElement(SKCanvas canvas, XElement el, SvgStyle inheri var w = Len(el, "width", state, style, LenAxis.X); var h = Len(el, "height", state, style, LenAxis.Y); if (!(w > 0) || !(h > 0)) return null; var rx = Len(el, "rx", state, style, LenAxis.X); var ry = Len(el, "ry", state, style, LenAxis.Y); - var path = new SKPath(); - if (rx > 0 || ry > 0) path.AddRoundRect(new SKRect((float)x, (float)y, (float)(x + w), (float)(y + h)), (float)(rx > 0 ? rx : ry), (float)(ry > 0 ? ry : rx)); - else path.AddRect(new SKRect((float)x, (float)y, (float)(x + w), (float)(y + h))); - return path; + using var builder = new SKPathBuilder(); + var rect = new SKRect((float)x, (float)y, (float)(x + w), (float)(y + h)); + if (rx > 0 || ry > 0) + builder.AddRoundRect(rect, (float)(rx > 0 ? rx : ry), (float)(ry > 0 ? ry : rx), SKPathDirection.Clockwise); + else + builder.AddRect(rect, SKPathDirection.Clockwise); + return builder.Detach(); } case "circle": { var r = Len(el, "r", state, style, LenAxis.Other); if (!(r > 0)) return null; - var path = new SKPath(); - path.AddCircle((float)Len(el, "cx", state, style, LenAxis.X), (float)Len(el, "cy", state, style, LenAxis.Y), (float)r); - return path; + using var builder = new SKPathBuilder(); + builder.AddCircle((float)Len(el, "cx", state, style, LenAxis.X), (float)Len(el, "cy", state, style, LenAxis.Y), (float)r, SKPathDirection.Clockwise); + return builder.Detach(); } case "ellipse": { var rx = Len(el, "rx", state, style, LenAxis.X); var ry = Len(el, "ry", state, style, LenAxis.Y); if (!(rx > 0) || !(ry > 0)) return null; var cx = Len(el, "cx", state, style, LenAxis.X); var cy = Len(el, "cy", state, style, LenAxis.Y); - var path = new SKPath(); - path.AddOval(new SKRect((float)(cx - rx), (float)(cy - ry), (float)(cx + rx), (float)(cy + ry))); - return path; + using var builder = new SKPathBuilder(); + builder.AddOval(new SKRect((float)(cx - rx), (float)(cy - ry), (float)(cx + rx), (float)(cy + ry)), SKPathDirection.Clockwise); + return builder.Detach(); } case "line": { - var path = new SKPath(); - path.MoveTo((float)Len(el, "x1", state, style, LenAxis.X), (float)Len(el, "y1", state, style, LenAxis.Y)); - path.LineTo((float)Len(el, "x2", state, style, LenAxis.X), (float)Len(el, "y2", state, style, LenAxis.Y)); - return path; + using var builder = new SKPathBuilder(); + builder.MoveTo((float)Len(el, "x1", state, style, LenAxis.X), (float)Len(el, "y1", state, style, LenAxis.Y)); + builder.LineTo((float)Len(el, "x2", state, style, LenAxis.X), (float)Len(el, "y2", state, style, LenAxis.Y)); + return builder.Detach(); } case "polyline": case "polygon": { var pts = ParsePoints(Attr(el, "points")); if (pts.Count < 2) return null; - var path = new SKPath(); - path.MoveTo(pts[0]); - for (var i = 1; i < pts.Count; i++) path.LineTo(pts[i]); - if (el.Name.LocalName.Equals("polygon", StringComparison.OrdinalIgnoreCase)) path.Close(); - return path; + using var builder = new SKPathBuilder(); + builder.MoveTo(pts[0]); + for (var i = 1; i < pts.Count; i++) builder.LineTo(pts[i]); + if (el.Name.LocalName.Equals("polygon", StringComparison.OrdinalIgnoreCase)) builder.Close(); + return builder.Detach(); } case "path": { @@ -429,7 +432,7 @@ private static void DrawImage(SKCanvas canvas, XElement el, SvgStyle style, SvgR var dest = new SKRect(x, y, x + (float)w, y + (float)h); if (image.Width <= 0 || image.Height <= 0) { - canvas.DrawImage(image, dest); + canvas.DrawImage(image, dest, new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.None), null); return; } // preserveAspectRatio (§8.8) — none stretches; meet fits inside; slice covers + clips to the rect. @@ -441,11 +444,11 @@ private static void DrawImage(SKCanvas canvas, XElement el, SvgStyle style, SvgR { var save = canvas.Save(); canvas.ClipRect(dest); - canvas.DrawImage(image, fitted); + canvas.DrawImage(image, fitted, new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.None), null); canvas.RestoreToCount(save); } else - canvas.DrawImage(image, fitted); + canvas.DrawImage(image, fitted, new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.None), null); } } diff --git a/src/NetPdf.Svg/SvgText.cs b/src/NetPdf.Svg/SvgText.cs index 437274e2..42212e54 100644 --- a/src/NetPdf.Svg/SvgText.cs +++ b/src/NetPdf.Svg/SvgText.cs @@ -354,17 +354,17 @@ private static void DrawPathGlyph( if (fillShader is not null) { using var p = new SKPaint { Style = SKPaintStyle.Fill, Shader = fillShader, IsAntialias = true }; - canvas.DrawText(glyph, x, y, font, p); + canvas.DrawText(glyph, x, y, SKTextAlign.Left, font, p); } else if (style.Fill.Alpha > 0 && style.FillOpacity > 0) { using var p = new SKPaint { Style = SKPaintStyle.Fill, Color = WithOpacity(style.Fill, style.FillOpacity), IsAntialias = true }; - canvas.DrawText(glyph, x, y, font, p); + canvas.DrawText(glyph, x, y, SKTextAlign.Left, font, p); } if (style.StrokeWidth > 0 && style.Stroke is { } sc && style.StrokeRef is null) { using var p = new SKPaint { Style = SKPaintStyle.Stroke, StrokeWidth = style.StrokeWidth, Color = WithOpacity(sc, style.StrokeOpacity), IsAntialias = true }; - canvas.DrawText(glyph, x, y, font, p); + canvas.DrawText(glyph, x, y, SKTextAlign.Left, font, p); } } @@ -401,13 +401,13 @@ private static void DrawRun( if (state.ResolveShader(fref, bounds, style.FillOpacity, style) is { } rp) using (rp) using (var p = new SKPaint { Style = SKPaintStyle.Fill, Shader = rp.Shader, IsAntialias = true }) - canvas.DrawText(textRun, x, baseline, font, p); + canvas.DrawText(textRun, x, baseline, SKTextAlign.Left, font, p); else state.SawUnsupported = true; } else if (style.Fill.Alpha > 0 && style.FillOpacity > 0) { using var p = new SKPaint { Style = SKPaintStyle.Fill, Color = WithOpacity(style.Fill, style.FillOpacity), IsAntialias = true }; - canvas.DrawText(textRun, x, baseline, font, p); + canvas.DrawText(textRun, x, baseline, SKTextAlign.Left, font, p); } if (style.StrokeWidth > 0) @@ -417,13 +417,13 @@ private static void DrawRun( if (state.ResolveShader(sref, bounds, style.StrokeOpacity, style) is { } rp) using (rp) using (var p = new SKPaint { Style = SKPaintStyle.Stroke, StrokeWidth = style.StrokeWidth, Shader = rp.Shader, IsAntialias = true }) - canvas.DrawText(textRun, x, baseline, font, p); + canvas.DrawText(textRun, x, baseline, SKTextAlign.Left, font, p); else state.SawUnsupported = true; } else if (style.Stroke is { } sc) { using var p = new SKPaint { Style = SKPaintStyle.Stroke, StrokeWidth = style.StrokeWidth, Color = WithOpacity(sc, style.StrokeOpacity), IsAntialias = true }; - canvas.DrawText(textRun, x, baseline, font, p); + canvas.DrawText(textRun, x, baseline, SKTextAlign.Left, font, p); } } } diff --git a/tests/NetPdf.UnitTests/Docs/ReleaseVersionParityTests.cs b/tests/NetPdf.UnitTests/Docs/ReleaseVersionParityTests.cs index f07b97de..6033c260 100644 --- a/tests/NetPdf.UnitTests/Docs/ReleaseVersionParityTests.cs +++ b/tests/NetPdf.UnitTests/Docs/ReleaseVersionParityTests.cs @@ -85,6 +85,48 @@ static bool TagRefMatches(string endpoint, string headingVersion) => $"'{previous}' (tag may be '{previous}' or 'v{previous}'). It should compare '{previous}...{latest}'."); } + [Fact] + public void Release_metadata_carries_no_version_specific_language() + { + // Per PR #355 review [P2] — the OTHER half of version drift: metadata that carries a version-specific + // CLAIM rather than a version NUMBER. `PackageReleaseNotes` said "First stable release." and shipped + // that text unchanged inside the 1.1.0 .nupkg, and build/version.json's `phaseDescription` still read + // "Packaging & release (v1.0 launch)" at version 1.1.0. Neither surface is covered by the parity test + // above (which only compares version NUMBERS), so both drifted silently across a release. + // + // These two fields are deliberately release-AGNOSTIC — they are written once and never bumped. This + // test pins that intent: no semver-ish token, no "first/initial release" ordinal claim. If you need to + // say something version-specific, say it in CHANGELOG.md, which is versioned by design. + var root = RepoRoot(); + + var notes = Regex.Match( + File.ReadAllText(Path.Combine(root, "Directory.Build.props")), + @"(.*?)", RegexOptions.Singleline).Groups[1].Value; + Assert.True(notes.Length > 0, "Directory.Build.props is missing ."); + AssertReleaseAgnostic(notes, "Directory.Build.props "); + + using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(root, "build", "version.json"))); + var phase = doc.RootElement.GetProperty("phaseDescription").GetString() ?? string.Empty; + AssertReleaseAgnostic(phase, "build/version.json phaseDescription"); + } + + /// Fails when pins itself to one release — either a semver-ish token + /// (1.0, v1.0.0) or an ordinal-release claim (first stable release). URLs are + /// exempt: the CHANGELOG link is the whole point, and its path carries no version. + private static void AssertReleaseAgnostic(string text, string surface) + { + var version = Regex.Match(text, @"\bv?\d+\.\d+(\.\d+)?\b"); + Assert.True(!version.Success, + $"{surface} carries the version-specific token '{version.Value}'. This field is written once and " + + "never bumped, so it silently ships stale on the next release — put version-specific wording in " + + "CHANGELOG.md instead."); + + var ordinal = Regex.Match(text, @"\b(first|initial)\s+(stable\s+)?release\b", RegexOptions.IgnoreCase); + Assert.True(!ordinal.Success, + $"{surface} carries the version-specific claim '{ordinal.Value}'. It was true for exactly one " + + "release and ships stale on every one after it — put it in CHANGELOG.md instead."); + } + /// All versioned `## [X] — …` headings in file order (newest first), excluding `[Unreleased]`. private static System.Collections.Generic.List ReadChangelogVersions(string path) { diff --git a/tests/NetPdf.UnitTests/HtmlPdfFacadeTests.cs b/tests/NetPdf.UnitTests/HtmlPdfFacadeTests.cs index b4cf5d41..0c584037 100644 --- a/tests/NetPdf.UnitTests/HtmlPdfFacadeTests.cs +++ b/tests/NetPdf.UnitTests/HtmlPdfFacadeTests.cs @@ -43,7 +43,7 @@ public void Version_reports_the_package_informational_version_not_the_assembly_v // would also pass `1.0.10` or an unrelated string carrying the fragment (PR-258 Copilot). Stable // releases from 1.0.0 on carry no prerelease suffix. Bump this on each release (guarded by // ReleaseVersionParityTests, which keeps the three version surfaces in agreement). - Assert.Matches(@"^1\.0\.2(\+.*)?$", version); + Assert.Matches(@"^1\.1\.0(\+.*)?$", version); } [Fact] diff --git a/tests/NetPdf.UnitTests/Pdf/Images/TextShadowRasterizerTests.cs b/tests/NetPdf.UnitTests/Pdf/Images/TextShadowRasterizerTests.cs index f7f11d39..11bd2645 100644 --- a/tests/NetPdf.UnitTests/Pdf/Images/TextShadowRasterizerTests.cs +++ b/tests/NetPdf.UnitTests/Pdf/Images/TextShadowRasterizerTests.cs @@ -16,7 +16,8 @@ public sealed class TextShadowRasterizerTests private static byte[] FontBytes(out ushort glyphA) { using var tf = SKTypeface.Default; - glyphA = tf.GetGlyph('A'); + using var font = new SKFont(tf); + glyphA = font.GetGlyph('A'); using var stream = tf.OpenStream(out _); var buffer = new byte[stream.Length]; stream.Read(buffer, buffer.Length);