Skip to content

Replace ImageSharp with SkiaSharp in the thumbnail generator - #6598

Merged
rbuergi merged 1 commit into
mainfrom
fix/replace-imagesharp-with-skiasharp
Oct 12, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/replace-imagesharp-with-skiasharp

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Maintainer decision: replace ImageSharp in core.

SixLabors.ImageSharp had exactly one consumer, tools/MeshWeaver.ThumbnailGenerator. Every version up to 4.1.1 carries three high-severity advisories (GHSA-j3p4-wp97-rph4, GHSA-j9gm-c75j-xc9q, GHSA-jjfr-hcj7-qf5w), and the only patched releases (4.1.2, and the 3.2.0 backport) enforce a paid licence in MSBuild. So the package goes, rather than being held.

What changes

  • ThumbnailResizer (new, in the tool): the box-fitting maths moved here unchanged, and the scale-down is now SkiaSharp (MIT), already shipped by the portal for share cards and favicons. It still scales the 2x Playwright capture to exactly the fitted size, using a bicubic (Catmull-Rom) sampler from the same family as ImageSharp's default Resize, and writes a PNG. Output size, format and filter family are unchanged.
  • The tool references SkiaSharp + SkiaSharp.NativeAssets.Linux.NoDependencies, both already centrally pinned.
  • SixLabors.ImageSharp is removed from Directory.Packages.props, and its Dependabot ignore block is removed from .github/dependabot.yml.
  • test/MeshWeaver.ThumbnailGenerator.Test (new): an 800x450 fixture capture becomes a 400x225 PNG. The test checks the encoded format, the dimensions, and that the pixels are not a single flat colour. It also covers aspect fitting (wider, taller, same aspect) and refuses an undecodable source by name. The project is in MeshWeaver.slnx, so shard-assign.sh picks it up at the default weight, and it is excluded from the global MeshWeaver.Messaging using, since it references no messaging.
  • HandWovenGateRatchetGuard: the reason text for the tool's sanctioned entry now names SkiaSharp. The tool still references no MeshWeaver assembly, so the entry's premise still holds.
  • The tool's README now describes how a thumbnail is made and why it uses SkiaSharp.

Nothing in CI invokes the generator. It is run from source (dotnet run --project tools/MeshWeaver.ThumbnailGenerator), per NuGetPackageRetirement, and that command is unchanged.

Coordination with #6549

Dependabot's group PR #6549 bumps ImageSharp 3.1.12 → 3.2.0 and moves its ignore to >=3.2.0. This PR supersedes that ImageSharp part: once this merges, both the pin and the ignore block are gone, and Dependabot's rebase of #6549 drops the ImageSharp bump. Its Testcontainers/xunit bumps are unaffected.

Verification (local, CI flags)

  • dotnet build -c Release -warnaserror test/MeshWeaver.ThumbnailGenerator.Test/... (builds the tool too): 0 warnings, 0 errors.
  • dotnet test on it: 5 of 5 passed.
  • dotnet build -c Release -warnaserror test/MeshWeaver.Documentation.Test/...: 0/0. HandWovenGate*: 4 of 4 passed.

Satellite-pins: SixLabors.ImageSharp — git grep of origin/main in MeshWeaver.Plugins, .Education, .Reinsurance, .SocialMedia, .Manufacturing, .Crm, .FundReporting and Memex: no csproj/props/targets references it versionless. Memex declares its own PackageVersion (3.1.12) in its own Directory.Packages.props and references it from no project; MeshWeaver.Plugins mentions it only in prose (src/MeshWeaver.AI/Data/Skill/package-versions.md).

Pairs-with: none — the removed package was referenced only by a standalone tool; no public type or member leaves src/.

🤖 Generated with Claude Code

ImageSharp's only consumer was tools/MeshWeaver.ThumbnailGenerator. Every version up to 4.1.1
carries three high-severity advisories and the patched line requires a paid licence, so the
generator now scales its captures with SkiaSharp (MIT), already shipped by the portal.

- ThumbnailResizer: box fitting (unchanged maths) + bicubic (Catmull-Rom) PNG scale to the exact
  thumbnail size, matching ImageSharp's default Resize sampler family.
- Drop SixLabors.ImageSharp from Directory.Packages.props and its Dependabot ignore block.
- New test/MeshWeaver.ThumbnailGenerator.Test: an 800x450 fixture capture becomes a 400x225 PNG.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 16:05
@rbuergi
rbuergi enabled auto-merge October 12, 2026 16:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Validation misidentifies invalid heights, and the repeated dependency rationale incorrectly includes patched ImageSharp 3.2.0 among vulnerable releases.

4 open findings
What changed in this PR

Replaces ImageSharp with the existing SkiaSharp stack in the standalone thumbnail generator.

Changes:

  • Adds SkiaSharp-based PNG resizing and aspect fitting.
  • Adds focused resizing tests and fixture coverage.
  • Removes ImageSharp dependency and Dependabot configuration.
File Description
.github/​dependabot.yml Removes the ImageSharp ignore rule.
Directory.Packages.props Removes the ImageSharp version pin.
MeshWeaver.slnx Adds the thumbnail test project.
tools/​MeshWeaver.ThumbnailGenerator/​ThumbnailResizer.cs Implements SkiaSharp resizing.
tools/​MeshWeaver.ThumbnailGenerator/​ThumbnailGenerator.cs Uses the new resizer.
tools/​MeshWeaver.ThumbnailGenerator/​MeshWeaver.ThumbnailGenerator.csproj Replaces package references.
tools/​MeshWeaver.ThumbnailGenerator/​README.md Documents thumbnail generation.
test/​MeshWeaver.ThumbnailGenerator.Test/​ThumbnailResizerTest.cs Tests dimensions, format, pixels, and failures.
test/​MeshWeaver.ThumbnailGenerator.Test/​MeshWeaver.ThumbnailGenerator.Test.csproj Defines the test project.
test/​MeshWeaver.ThumbnailGenerator.Test/​Fixtures/​screenshot-800x450.png Supplies the resize fixture.
test/​MeshWeaver.Documentation.Test/​HandWovenGateRatchetGuard.cs Updates the sanctioned-tool rationale.
test/​Directory.Build.props Excludes the dependency-free test from messaging imports.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +37 to +41
var distinct = new HashSet<SKColor>();
for (var y = 0; y < bitmap.Height; y += 15)
for (var x = 0; x < bitmap.Width; x += 20)
distinct.Add(bitmap.GetPixel(x, y));
Assert.True(distinct.Count > 1, "The thumbnail is a single flat colour — nothing of the capture survived.");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point on convention, but not a defect worth a new review round: the set is a method-local, single-threaded scratch value inside a test assertion and never escapes it; the immutable-collections rule targets state that is shared or held. The assertion's result (more than one distinct sampled colour) is the same either way. Leaving it as is under this PR's converge rule (push only for a wrong result, security hole, data loss or broken build/test).

Comment on lines +16 to +19
<!-- Image scaling: SkiaSharp (MIT), the stack the portal already ships for share cards and
favicons. Replaced SixLabors.ImageSharp — every version up to 4.1.1 carries high-severity
advisories and the patched line is pay-to-use. The Linux natives let the tool (and its
test) run on a Linux runner; macOS and Windows natives come with SkiaSharp itself. -->

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the phrase "every version up to 4.1.1" is loose: the 3.x line's patched backport is 3.2.0, so precisely the vulnerable set is < 3.2.0 and 4.0.0–4.1.1, with 3.2.0 and 4.1.2 patched but licence-enforcing. The PR body states that precisely, and the decision does not change: both patched releases enforce a paid licence in MSBuild, which is why the package goes. This is wording in a comment/README, not behaviour, so it is not pushed on its own under this PR's converge rule; it can be tightened in a follow-up touching the tool.

Comment on lines +47 to +49
the same stack the portal ships for share cards and favicons. It replaced SixLabors.ImageSharp:
every ImageSharp version up to 4.1.1 carries high-severity advisories, and the patched releases
need a paid licence. `test/MeshWeaver.ThumbnailGenerator.Test` pins the output size and format. No newline at end of file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the phrase "every version up to 4.1.1" is loose: the 3.x line's patched backport is 3.2.0, so precisely the vulnerable set is < 3.2.0 and 4.0.0–4.1.1, with 3.2.0 and 4.1.2 patched but licence-enforcing. The PR body states that precisely, and the decision does not change: both patched releases enforce a paid licence in MSBuild, which is why the package goes. This is wording in a comment/README, not behaviour, so it is not pushed on its own under this PR's converge rule; it can be tightened in a follow-up touching the tool.

Comment on lines +8 to +9
/// ships for its share cards and favicons; SixLabors.ImageSharp was dropped because every version up
/// to 4.1.1 carries high-severity advisories and the patched line needs a paid licence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the phrase "every version up to 4.1.1" is loose: the 3.x line's patched backport is 3.2.0, so precisely the vulnerable set is < 3.2.0 and 4.0.0–4.1.1, with 3.2.0 and 4.1.2 patched but licence-enforcing. The PR body states that precisely, and the decision does not change: both patched releases enforce a paid licence in MSBuild, which is why the package goes. This is wording in a comment/README, not behaviour, so it is not pushed on its own under this PR's converge rule; it can be tightened in a follow-up touching the tool.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    25 files  + 1      25 suites  +1   57m 4s ⏱️ -3s
12 202 tests +31  12 009 ✅ +31  193 💤 ±0  0 ❌ ±0 
12 216 runs  +31  12 023 ✅ +31  193 💤 ±0  0 ❌ ±0 

Results for commit 6258336. ± Comparison against base commit 5ba08db.

This pull request removes 40 and adds 47 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "4fde973b-d717-49c5-86fe-82ee55c43a88")
Memex.Portal.Shared.Test.SearchReportsPartialReadsTest ‑ APartialVerdict_NamesThePartition_AndCarriesTheHint
Memex.Portal.Shared.Test.SearchReportsPartialReadsTest ‑ AnAssessedCoveredVerdict_IsEmpty_AndCarriesNoHint
Memex.Portal.Shared.Test.SearchReportsPartialReadsTest ‑ AnUnassessedVerdict_IsNull_NotEmpty
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
 ---> System.UnauthorizedAccessException: Access denied
   --- End of inner exception stack trace ---, isDenial: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's FIRST member", fault: System.AggregateException: One or more errors occurred. (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's SECOND member", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall nested in an aggregate inside a wrapper", fault: System.InvalidOperationException: outer
 ---> System.AggregateException: One or more errors occurred. (Value does not fall within the expected range.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.ArgumentException: Value does not fall within the expected range.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---

   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall wrapped as InnerException", fault: System.InvalidOperationException: outer
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "an aggregate of unrelated faults", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Value does not fall within the expected range.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
, expected: False)
…

♻️ This comment has been updated with latest results.

@rbuergi
rbuergi merged commit 2c7cf78 into main Oct 12, 2026
101 of 108 checks passed
rbuergi added a commit that referenced this pull request Oct 13, 2026
main replaced SixLabors.ImageSharp with SkiaSharp (#6598), so the ImageSharp hold this branch
carried in Directory.Packages.props and .github/dependabot.yml is dropped with the package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants