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);