diff --git a/lint.ps1 b/lint.ps1
index 89b344dc..a2bf2703 100644
--- a/lint.ps1
+++ b/lint.ps1
@@ -136,8 +136,63 @@ dotnet restore > $null
if ($LASTEXITCODE -ne 0) { $lintError = $true; $skipDotnetFormat = $true }
if (-not $skipDotnetFormat) {
- dotnet format --verify-no-changes --no-restore
- if ($LASTEXITCODE -ne 0) { $lintError = $true }
+ # NOTE: `dotnet format --verify-no-changes` has a known cross-platform exit-code
+ # bug (e.g. dotnet/sdk#41422) where it can report a non-zero exit code even when
+ # zero files require formatting, inconsistently between operating systems. To get
+ # a reliable check, format in place and use `git diff` as the source of truth.
+ #
+ # A plain `git diff` after formatting would also include any pre-existing
+ # uncommitted *.cs changes unrelated to formatting (e.g. a developer's
+ # in-progress edits), causing false failures. Stash those away first so the
+ # formatter runs against a clean tree and the diff reflects only its own
+ # changes, then restore the original working tree afterward regardless of
+ # outcome, leaving no side effects from running this check.
+ #
+ # `git status --porcelain` (rather than `git diff`) is used to detect
+ # pre-existing changes so that untracked *.cs files are also caught and
+ # stashed - otherwise an untracked file reformatted by `dotnet format`
+ # would be left modified with no diff ever having been checked against it.
+ # `--index` on the pop restores the original staged/unstaged split rather
+ # than flattening everything into the working tree.
+ $statusBeforeFormat = git status --porcelain -- '*.cs'
+ $hasPreexistingCsChanges = [bool]$statusBeforeFormat
+ if ($hasPreexistingCsChanges) {
+ git stash push --include-untracked --quiet --message "lint.ps1: pre-existing *.cs changes" -- '*.cs'
+ if ($LASTEXITCODE -ne 0) {
+ $lintError = $true
+ $skipDotnetFormat = $true
+ Write-Host "Failed to stash pre-existing *.cs changes; skipping dotnet format check."
+ $hasPreexistingCsChanges = $false
+ }
+ }
+}
+
+if (-not $skipDotnetFormat) {
+ dotnet format --no-restore
+ if ($LASTEXITCODE -ne 0) {
+ $lintError = $true
+ }
+ else {
+ git diff --exit-code -- '*.cs'
+ if ($LASTEXITCODE -ne 0) {
+ $lintError = $true
+ Write-Host "dotnet format made changes; run fix.ps1 locally and commit the results."
+ }
+ }
+
+ # Restore the working tree to its original state: discard any in-place
+ # formatting changes made purely for this check, then reapply the
+ # developer's original uncommitted *.cs changes (if any were stashed).
+ # `--index` restores the original staged/unstaged split rather than
+ # dropping everything into the working tree unstaged.
+ git checkout -- '*.cs' 2>$null
+ if ($hasPreexistingCsChanges) {
+ git stash pop --index --quiet
+ if ($LASTEXITCODE -ne 0) {
+ $lintError = $true
+ Write-Host "Failed to restore stashed *.cs changes; check 'git stash list' and resolve manually."
+ }
+ }
}
# [PROJECT-SPECIFIC] Add additional format verification checks here.
diff --git a/src/DemaConsulting.CanvasNet/Canvas/Rgba32.cs b/src/DemaConsulting.CanvasNet/Canvas/Rgba32.cs
index 7b74ee95..1c53d97f 100644
--- a/src/DemaConsulting.CanvasNet/Canvas/Rgba32.cs
+++ b/src/DemaConsulting.CanvasNet/Canvas/Rgba32.cs
@@ -238,6 +238,11 @@ private static bool TryParseCore(string s, out Rgba32 result, out string? reason
}
/// Returns whether is a valid ASCII hexadecimal digit.
- private static bool IsHexDigit(char c) =>
- (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
+ ///
+ /// Delegates to rather than a hand-written combination
+ /// of range comparisons - it is exactly equivalent (true for '0'-'9',
+ /// 'a'-'f', and 'A'-'F') while avoiding a hard-to-scan, multi-operator
+ /// boolean expression.
+ ///
+ private static bool IsHexDigit(char c) => char.IsAsciiHexDigit(c);
}
diff --git a/src/DemaConsulting.CanvasNet/Canvas/Surface.cs b/src/DemaConsulting.CanvasNet/Canvas/Surface.cs
index ce9b2186..21f4502d 100644
--- a/src/DemaConsulting.CanvasNet/Canvas/Surface.cs
+++ b/src/DemaConsulting.CanvasNet/Canvas/Surface.cs
@@ -768,7 +768,7 @@ public void CompositeOverSpan(int y, int x, ReadOnlySpan coverage, Rgba32
using var fg = new RowChannelBuffers(count);
using var work = new CompositeWorkBuffers(count);
- CompositeOverSpanCore(y, x, coverage, color, bg, fg, work, count);
+ CompositeOverSpanCore(y, x, coverage, color, bg, fg, work);
}
///
@@ -825,7 +825,7 @@ internal void CompositeOverSpan(int y, int x, ReadOnlySpan coverage, Rgba
nameof(coverage), coverage.Length, "Coverage length must not exceed the workspace's capacity.");
}
- CompositeOverSpanCore(y, x, coverage, color, workspace.Bg, workspace.Fg, workspace.Work, count);
+ CompositeOverSpanCore(y, x, coverage, color, workspace.Bg, workspace.Fg, workspace.Work);
}
///
@@ -886,7 +886,7 @@ public void CompositeOverSpan(int y, int x, ReadOnlySpan coverage, ReadOn
using var fg = new RowChannelBuffers(count);
using var work = new CompositeWorkBuffers(count);
- CompositeOverSpanCore(y, x, coverage, colors, bg, fg, work, count);
+ CompositeOverSpanCore(y, x, coverage, colors, bg, fg, work);
}
///
@@ -945,7 +945,7 @@ internal void CompositeOverSpan(int y, int x, ReadOnlySpan coverage, Read
nameof(coverage), coverage.Length, "Coverage length must not exceed the workspace's capacity.");
}
- CompositeOverSpanCore(y, x, coverage, colors, workspace.Bg, workspace.Fg, workspace.Work, count);
+ CompositeOverSpanCore(y, x, coverage, colors, workspace.Bg, workspace.Fg, workspace.Work);
}
///
@@ -988,8 +988,9 @@ private void ValidateCompositeOverSpanArgs(int y, int x, int coverageLength)
///
private void CompositeOverSpanCore(
int y, int x, ReadOnlySpan coverage, Rgba32 color, RowChannelBuffers bg, RowChannelBuffers fg,
- CompositeWorkBuffers work, int count)
+ CompositeWorkBuffers work)
{
+ var count = coverage.Length;
for (var i = 0; i < count; i++)
{
fg.RBytes[i] = color.R;
@@ -999,19 +1000,20 @@ private void CompositeOverSpanCore(
fg.ABytes[i] = (byte)Math.Clamp(MathF.Round(scaledAlpha, MidpointRounding.AwayFromZero), 0f, 255f);
}
- CompositeOverSpanCoreShared(y, x, coverage, bg, fg, work, count);
+ CompositeOverSpanCoreShared(y, x, coverage, bg, fg, work);
}
///
- /// Per-pixel-color counterpart of :
+ /// Per-pixel-color counterpart of :
/// populates from one per pixel in
/// , rather than broadcasting a single constant color, then
/// shares the exact same blend body.
///
private void CompositeOverSpanCore(
int y, int x, ReadOnlySpan coverage, ReadOnlySpan colors, RowChannelBuffers bg, RowChannelBuffers fg,
- CompositeWorkBuffers work, int count)
+ CompositeWorkBuffers work)
{
+ var count = coverage.Length;
for (var i = 0; i < count; i++)
{
var color = colors[i];
@@ -1022,7 +1024,7 @@ private void CompositeOverSpanCore(
fg.ABytes[i] = (byte)Math.Clamp(MathF.Round(scaledAlpha, MidpointRounding.AwayFromZero), 0f, 255f);
}
- CompositeOverSpanCoreShared(y, x, coverage, bg, fg, work, count);
+ CompositeOverSpanCoreShared(y, x, coverage, bg, fg, work);
}
///
@@ -1032,12 +1034,16 @@ private void CompositeOverSpanCore(
/// pipeline, and writes the result back into the surface
/// via . must already be populated (its
/// byte channels only - this method widens it to float itself) by the caller before this
- /// method is invoked.
+ /// method is invoked. 's length is always the row's active
+ /// pixel count - it is not passed separately since every caller derives it from the very
+ /// same span before renting //
+ /// to that exact size.
///
private void CompositeOverSpanCoreShared(
int y, int x, ReadOnlySpan coverage, RowChannelBuffers bg, RowChannelBuffers fg,
- CompositeWorkBuffers work, int count)
+ CompositeWorkBuffers work)
{
+ var count = coverage.Length;
var bgRow = GetRowSpanBytes(y).Slice(x * BytesPerPixel, count * BytesPerPixel);
DeinterleaveRow(bgRow, bg, count);
@@ -1197,9 +1203,9 @@ private static void CompositeOverRow(RowChannelBuffers bg, RowChannelBuffers fg,
TensorPrimitives.Multiply(bgAn, oneMinusFgAn, outAn);
TensorPrimitives.Add(outAn, fgAn, outAn);
- CompositeOverChannel(bg.RFloat, fg.RFloat, bgAn, fgAn, oneMinusFgAn, outAn, work.Term, work.OutR, count);
- CompositeOverChannel(bg.GFloat, fg.GFloat, bgAn, fgAn, oneMinusFgAn, outAn, work.Term, work.OutG, count);
- CompositeOverChannel(bg.BFloat, fg.BFloat, bgAn, fgAn, oneMinusFgAn, outAn, work.Term, work.OutB, count);
+ CompositeOverChannel(bg.RFloat, fg.RFloat, work, work.OutR, count);
+ CompositeOverChannel(bg.GFloat, fg.GFloat, work, work.OutG, count);
+ CompositeOverChannel(bg.BFloat, fg.BFloat, work, work.OutB, count);
// Only now scale the normalized outA up to [0, 255] for narrowing - every channel above
// divided by the still-normalized outAn.
@@ -1208,17 +1214,22 @@ private static void CompositeOverRow(RowChannelBuffers bg, RowChannelBuffers fg,
///
/// Computes (fgC * fgA + bgC * bgA * (1 - fgA)) / outA for one [0, 255]-
- /// scaled color channel of a whole row, where the alpha terms (,
- /// , ) are [0, 1]-scaled.
+ /// scaled color channel of a whole row, reading the alpha terms (BgAn/FgAn/
+ /// OneMinusFgA/OutA, all [0, 1]-scaled) and the Term scratch
+ /// buffer directly from - the same
+ /// instance already
+ /// populated them into, rather than re-passing each of its fields as its own parameter.
///
- private static void CompositeOverChannel(
- float[] bgC, float[] fgC, Span bgAn, Span fgAn, Span oneMinusFgAn, Span outAn,
- float[] termScratch, float[] outC, int count)
+ private static void CompositeOverChannel(float[] bgC, float[] fgC, CompositeWorkBuffers work, float[] outC, int count)
{
+ var bgAn = work.BgAn.AsSpan(0, count);
+ var fgAn = work.FgAn.AsSpan(0, count);
+ var oneMinusFgAn = work.OneMinusFgA.AsSpan(0, count);
+ var outAn = work.OutA.AsSpan(0, count);
var bgCSpan = bgC.AsSpan(0, count);
var fgCSpan = fgC.AsSpan(0, count);
var outCSpan = outC.AsSpan(0, count);
- var termSpan = termScratch.AsSpan(0, count);
+ var termSpan = work.Term.AsSpan(0, count);
// outC = fgC * fgA
TensorPrimitives.Multiply(fgCSpan, fgAn, outCSpan);
diff --git a/src/DemaConsulting.CanvasNet/Codecs/GifCodec.cs b/src/DemaConsulting.CanvasNet/Codecs/GifCodec.cs
index 0098a026..d3024dae 100644
--- a/src/DemaConsulting.CanvasNet/Codecs/GifCodec.cs
+++ b/src/DemaConsulting.CanvasNet/Codecs/GifCodec.cs
@@ -290,27 +290,16 @@ public static Surface Load(Stream stream)
}
result = new Surface(canvasWidth, canvasHeight);
- for (var row = 0; row < imgHeight; row++)
- {
- var rowSpan = result.GetRowSpan(top + row);
- var rowOffset = row * imgWidth;
- for (var col = 0; col < imgWidth; col++)
- {
- var index = indices[rowOffset + col];
- if (index >= activeColorTable.Length)
- {
- throw new InvalidDataException(
- $"GIF pixel index {index} is out of range for its color table " +
- $"({activeColorTable.Length} entries).");
- }
-
- var color = activeColorTable[index];
- var alpha = (byte)(pendingTransparencyFlag && index == pendingTransparentIndex
- ? 0
- : 255);
- rowSpan[left + col] = new Rgba32(color.R, color.G, color.B, alpha);
- }
- }
+ BlitIndexedFrame(
+ result,
+ indices,
+ activeColorTable,
+ left,
+ top,
+ imgWidth,
+ imgHeight,
+ pendingTransparencyFlag,
+ pendingTransparentIndex);
}
pendingTransparencyFlag = false;
@@ -365,6 +354,60 @@ public static Surface Load(string path)
return Load(stream);
}
+ ///
+ /// Resolves one frame's decoded palette indices to RGBA pixels via
+ /// , writing them into at the
+ /// frame's declared placement, honoring the frame's transparent color index if any.
+ ///
+ ///
+ /// Isolated from as its own self-contained blit step - resolving
+ /// an indexed frame's pixels is a distinct, independently testable operation from the
+ /// surrounding GIF block/stream parsing that produces its inputs.
+ ///
+ /// The surface to write pixels into. Must already be sized to contain the frame's placement.
+ /// The frame's decoded palette indices, one per pixel, in row-major order.
+ /// The color table (local or global) to resolve each index against.
+ /// The frame's left placement offset within .
+ /// The frame's top placement offset within .
+ /// The frame's width, in pixels.
+ /// The frame's height, in pixels.
+ /// Whether should be rendered fully transparent.
+ /// The palette index treated as transparent when is set.
+ ///
+ /// Thrown when a decoded index has no corresponding entry in .
+ ///
+ private static void BlitIndexedFrame(
+ Surface destination,
+ byte[] indices,
+ Rgba32[] colorTable,
+ int left,
+ int top,
+ int width,
+ int height,
+ bool transparencyFlag,
+ byte transparentIndex)
+ {
+ for (var row = 0; row < height; row++)
+ {
+ var rowSpan = destination.GetRowSpan(top + row);
+ var rowOffset = row * width;
+ for (var col = 0; col < width; col++)
+ {
+ var index = indices[rowOffset + col];
+ if (index >= colorTable.Length)
+ {
+ throw new InvalidDataException(
+ $"GIF pixel index {index} is out of range for its color table " +
+ $"({colorTable.Length} entries).");
+ }
+
+ var color = colorTable[index];
+ var alpha = (byte)(transparencyFlag && index == transparentIndex ? 0 : 255);
+ rowSpan[left + col] = new Rgba32(color.R, color.G, color.B, alpha);
+ }
+ }
+ }
+
///
/// Reads a GIF file's Logical Screen Descriptor and reports its declared dimensions,
/// without reading any color table, block, or pixel data.
diff --git a/src/DemaConsulting.CanvasNet/Codecs/PngCodec.cs b/src/DemaConsulting.CanvasNet/Codecs/PngCodec.cs
index 397416f5..4037711f 100644
--- a/src/DemaConsulting.CanvasNet/Codecs/PngCodec.cs
+++ b/src/DemaConsulting.CanvasNet/Codecs/PngCodec.cs
@@ -357,10 +357,7 @@ public static Surface Load(Stream stream)
return DecodeScanlines(
rawData,
- header.Width,
- header.Height,
- header.ColorType,
- header.BitDepth,
+ header,
rowBytes,
bpp,
samplesPerPixel,
@@ -660,92 +657,11 @@ private static void ProcessChunk(Stream stream, MemoryStream idatStream, ChunkRe
}
else if (ChunkTypeIs(typeBytes, "PLTE"))
{
- if (state.PlteSeen)
- {
- throw new InvalidDataException("Duplicate PLTE chunk.");
- }
-
- if (state.IdatSeen)
- {
- throw new InvalidDataException("PLTE chunk encountered after the first IDAT chunk.");
- }
-
- // The PNG specification requires PLTE to precede tRNS whenever both are present,
- // regardless of color type: a tRNS chunk that has already been accepted means a PLTE
- // chunk arriving afterward is out of order, even for color types (2 and 6) where
- // PLTE is merely an optional suggested palette rather than mandatory
- if (state.TrnsSeen)
- {
- throw new InvalidDataException("PLTE chunk must precede tRNS chunk.");
- }
-
- // ValidateChunkLengthBeforeAllocation already bounds the declared length to at most
- // 256 entries (768 bytes), so only the multiple-of-3/non-empty shape remains to check
- if (data.Length % 3 != 0 || data.Length == 0)
- {
- throw new InvalidDataException(
- $"Invalid PNG PLTE chunk length {data.Length}; expected a positive multiple of 3.");
- }
-
- // The PNG specification forbids a PLTE chunk for the two grayscale color types (0 and
- // 4): grayscale samples are never resolved through a palette, so a PLTE chunk on such
- // a file cannot represent anything a conforming decoder is permitted to use
- if (state.ColorType is ColorTypeGrayscale or ColorTypeGrayscaleAlpha)
- {
- throw new InvalidDataException(
- $"PLTE chunk is not permitted for grayscale PNG color type {state.ColorType}.");
- }
-
- if (state.ColorType == ColorTypePalette)
- {
- var maxEntries = 1 << state.BitDepth;
- var entryCount = data.Length / 3;
- if (entryCount > maxEntries)
- {
- throw new InvalidDataException(
- $"PNG PLTE chunk declares {entryCount} palette entries, which exceeds the maximum of " +
- $"{maxEntries} entries permitted for a palette (color type 3) image at bit depth " +
- $"{state.BitDepth}.");
- }
- }
-
- state.PlteData = data;
- state.PlteSeen = true;
+ ProcessPlteChunk(data, state);
}
else if (ChunkTypeIs(typeBytes, "tRNS"))
{
- if (state.TrnsSeen)
- {
- throw new InvalidDataException("Duplicate tRNS chunk.");
- }
-
- if (state.IdatSeen)
- {
- throw new InvalidDataException("tRNS chunk encountered after the first IDAT chunk.");
- }
-
- // The PNG specification forbids a tRNS chunk for the two color types that already
- // carry a full per-pixel alpha channel (grayscale-with-alpha and Truecolor-with-alpha):
- // there is nothing for a single-key-color transparency chunk to add for those formats,
- // so its presence indicates a malformed, non-conforming file rather than a feature to
- // silently ignore
- if (state.ColorType is ColorTypeGrayscaleAlpha or ColorTypeTruecolorAlpha)
- {
- throw new InvalidDataException(
- $"tRNS chunk is not permitted for PNG color type {state.ColorType}, which already " +
- "carries a full per-pixel alpha channel.");
- }
-
- // For indexed-color (palette) images, tRNS's per-palette-entry alpha values are
- // meaningless without the PLTE chunk they index into, so the specification requires
- // tRNS to appear after PLTE, not merely after IHDR and before the first IDAT
- if (state.ColorType == ColorTypePalette && !state.PlteSeen)
- {
- throw new InvalidDataException("tRNS chunk for indexed-color PNG must follow PLTE.");
- }
-
- state.TrnsData = data;
- state.TrnsSeen = true;
+ ProcessTrnsChunk(data, state);
}
else if (ChunkTypeIs(typeBytes, "IDAT"))
{
@@ -772,6 +688,132 @@ private static void ProcessChunk(Stream stream, MemoryStream idatStream, ChunkRe
// all, so no further action is required.
}
+ ///
+ /// Validates and records a PLTE chunk's payload into ,
+ /// enforcing every content-dependent PLTE rule that requires the chunk's actual payload
+ /// (ordering relative to tRNS/IDAT, entry-count shape, and color-type/bit-depth
+ /// compatibility).
+ ///
+ ///
+ /// Isolated from as its own self-contained validation step,
+ /// independently nameable and testable from the surrounding chunk-type dispatch and from
+ /// 's equivalent tRNS validation.
+ ///
+ /// The chunk's payload bytes.
+ /// The in-progress chunk-read state, updated with the accepted palette data.
+ ///
+ /// Thrown for a duplicate PLTE chunk, a PLTE chunk following tRNS or the first IDAT, an
+ /// invalid entry-count shape, a PLTE chunk on a grayscale color type, or a PLTE chunk
+ /// declaring more entries than its bit depth permits.
+ ///
+ private static void ProcessPlteChunk(byte[] data, ChunkReadState state)
+ {
+ if (state.PlteSeen)
+ {
+ throw new InvalidDataException("Duplicate PLTE chunk.");
+ }
+
+ if (state.IdatSeen)
+ {
+ throw new InvalidDataException("PLTE chunk encountered after the first IDAT chunk.");
+ }
+
+ // The PNG specification requires PLTE to precede tRNS whenever both are present,
+ // regardless of color type: a tRNS chunk that has already been accepted means a PLTE
+ // chunk arriving afterward is out of order, even for color types (2 and 6) where
+ // PLTE is merely an optional suggested palette rather than mandatory
+ if (state.TrnsSeen)
+ {
+ throw new InvalidDataException("PLTE chunk must precede tRNS chunk.");
+ }
+
+ // ValidateChunkLengthBeforeAllocation already bounds the declared length to at most
+ // 256 entries (768 bytes), so only the multiple-of-3/non-empty shape remains to check
+ if (data.Length % 3 != 0 || data.Length == 0)
+ {
+ throw new InvalidDataException(
+ $"Invalid PNG PLTE chunk length {data.Length}; expected a positive multiple of 3.");
+ }
+
+ // The PNG specification forbids a PLTE chunk for the two grayscale color types (0 and
+ // 4): grayscale samples are never resolved through a palette, so a PLTE chunk on such
+ // a file cannot represent anything a conforming decoder is permitted to use
+ if (state.ColorType is ColorTypeGrayscale or ColorTypeGrayscaleAlpha)
+ {
+ throw new InvalidDataException(
+ $"PLTE chunk is not permitted for grayscale PNG color type {state.ColorType}.");
+ }
+
+ if (state.ColorType == ColorTypePalette)
+ {
+ var maxEntries = 1 << state.BitDepth;
+ var entryCount = data.Length / 3;
+ if (entryCount > maxEntries)
+ {
+ throw new InvalidDataException(
+ $"PNG PLTE chunk declares {entryCount} palette entries, which exceeds the maximum of " +
+ $"{maxEntries} entries permitted for a palette (color type 3) image at bit depth " +
+ $"{state.BitDepth}.");
+ }
+ }
+
+ state.PlteData = data;
+ state.PlteSeen = true;
+ }
+
+ ///
+ /// Validates and records a tRNS chunk's payload into ,
+ /// enforcing every content-dependent tRNS rule that requires the chunk's actual payload
+ /// (ordering relative to PLTE/IDAT and color-type compatibility).
+ ///
+ ///
+ /// Isolated from as its own self-contained validation step,
+ /// independently nameable and testable from the surrounding chunk-type dispatch and from
+ /// 's equivalent PLTE validation.
+ ///
+ /// The chunk's payload bytes.
+ /// The in-progress chunk-read state, updated with the accepted transparency data.
+ ///
+ /// Thrown for a duplicate tRNS chunk, a tRNS chunk following the first IDAT, a tRNS chunk
+ /// on a color type that already carries full per-pixel alpha, or an indexed-color tRNS
+ /// chunk preceding PLTE.
+ ///
+ private static void ProcessTrnsChunk(byte[] data, ChunkReadState state)
+ {
+ if (state.TrnsSeen)
+ {
+ throw new InvalidDataException("Duplicate tRNS chunk.");
+ }
+
+ if (state.IdatSeen)
+ {
+ throw new InvalidDataException("tRNS chunk encountered after the first IDAT chunk.");
+ }
+
+ // The PNG specification forbids a tRNS chunk for the two color types that already
+ // carry a full per-pixel alpha channel (grayscale-with-alpha and Truecolor-with-alpha):
+ // there is nothing for a single-key-color transparency chunk to add for those formats,
+ // so its presence indicates a malformed, non-conforming file rather than a feature to
+ // silently ignore
+ if (state.ColorType is ColorTypeGrayscaleAlpha or ColorTypeTruecolorAlpha)
+ {
+ throw new InvalidDataException(
+ $"tRNS chunk is not permitted for PNG color type {state.ColorType}, which already " +
+ "carries a full per-pixel alpha channel.");
+ }
+
+ // For indexed-color (palette) images, tRNS's per-palette-entry alpha values are
+ // meaningless without the PLTE chunk they index into, so the specification requires
+ // tRNS to appear after PLTE, not merely after IHDR and before the first IDAT
+ if (state.ColorType == ColorTypePalette && !state.PlteSeen)
+ {
+ throw new InvalidDataException("tRNS chunk for indexed-color PNG must follow PLTE.");
+ }
+
+ state.TrnsData = data;
+ state.TrnsSeen = true;
+ }
+
///
/// This codec's single authoritative gate for every chunk-ordering/identity rule that
/// does not depend on a chunk's payload content, invoked by
@@ -1248,30 +1290,25 @@ private static (byte[] TypeBytes, byte[] Data) ReadIhdrChunkFrame(Stream stream)
/// Defilters and decodes every scanline of decompressed PNG raw data into a new
/// , reconstructing each row from the previous row per the PNG
/// filtering specification, then mapping each row's samples to RGBA per
- /// .
+ /// 's color type.
///
/// The decompressed, filtered scanline bytes (one filter-type byte plus per row).
- /// The image width, in pixels.
- /// The image height, in pixels.
- /// The PNG color type.
- /// The PNG bit depth (1, 2, 4, 8, or 16).
+ /// The parsed IHDR fields (width, height, color type, and bit depth).
/// The number of packed pixel bytes per row (excluding the filter-type byte).
/// The number of whole bytes per pixel, used by the defilter algorithms (at least 1).
- /// The number of samples per pixel for .
+ /// The number of samples per pixel for 's color type.
/// The raw PLTE chunk data (RGB triples), or null if absent.
/// The validated tRNS chunk data for this color type, or null if absent/not applicable.
private static Surface DecodeScanlines(
byte[] rawData,
- int width,
- int height,
- int colorType,
- int bitDepth,
+ PngHeader header,
int rowBytes,
int bpp,
int samplesPerPixel,
byte[]? palette,
byte[]? trns)
{
+ var (width, height, colorType, bitDepth, _) = header;
var surface = new Surface(width, height);
var previousRow = new byte[rowBytes];
var currentRow = new byte[rowBytes];
@@ -1374,106 +1411,160 @@ private static void MapSamplesToRgba(
byte[]? trns,
Span destination)
{
- var maxSample = (1 << bitDepth) - 1;
-
switch (colorType)
{
case ColorTypeGrayscale:
- {
- var trnsGray = trns != null ? ReadUInt16Be(trns, 0) : -1;
- for (var x = 0; x < width; x++)
- {
- var raw = samples[x];
- var isTransparent = raw == trnsGray;
- var gray = bitDepth == 16 ? (byte)(raw >> 8) : (byte)(raw * 255 / maxSample);
- var d = x * 4;
- destination[d] = gray;
- destination[d + 1] = gray;
- destination[d + 2] = gray;
- destination[d + 3] = (byte)(isTransparent ? 0 : 255);
- }
-
- break;
- }
+ MapGrayscaleSamples(samples, width, bitDepth, trns, destination);
+ break;
case ColorTypeTruecolor:
- {
- var hasTrns = trns != null;
- var trnsR = hasTrns ? ReadUInt16Be(trns!, 0) : -1;
- var trnsG = hasTrns ? ReadUInt16Be(trns!, 2) : -1;
- var trnsB = hasTrns ? ReadUInt16Be(trns!, 4) : -1;
- for (var x = 0; x < width; x++)
- {
- var s = x * 3;
- var r = samples[s];
- var g = samples[s + 1];
- var b = samples[s + 2];
- var isTransparent = r == trnsR && g == trnsG && b == trnsB;
- var d = x * 4;
- destination[d] = bitDepth == 16 ? (byte)(r >> 8) : (byte)r;
- destination[d + 1] = bitDepth == 16 ? (byte)(g >> 8) : (byte)g;
- destination[d + 2] = bitDepth == 16 ? (byte)(b >> 8) : (byte)b;
- destination[d + 3] = (byte)(isTransparent ? 0 : 255);
- }
-
- break;
- }
+ MapTruecolorSamples(samples, width, bitDepth, trns, destination);
+ break;
case ColorTypePalette:
- {
- var entries = palette!.Length / 3;
- for (var x = 0; x < width; x++)
- {
- var index = samples[x];
- if (index >= entries)
- {
- throw new InvalidDataException(
- $"PNG palette index {index} is out of range for a {entries}-entry PLTE chunk.");
- }
-
- var p = index * 3;
- var d = x * 4;
- destination[d] = palette[p];
- destination[d + 1] = palette[p + 1];
- destination[d + 2] = palette[p + 2];
- destination[d + 3] = trns != null && index < trns.Length ? trns[index] : (byte)255;
- }
-
- break;
- }
+ MapPaletteSamples(samples, width, palette!, trns, destination);
+ break;
case ColorTypeGrayscaleAlpha:
- {
- for (var x = 0; x < width; x++)
- {
- var s = x * 2;
- var gray = samples[s];
- var alpha = samples[s + 1];
- var d = x * 4;
- var grayByte = bitDepth == 16 ? (byte)(gray >> 8) : (byte)gray;
- destination[d] = grayByte;
- destination[d + 1] = grayByte;
- destination[d + 2] = grayByte;
- destination[d + 3] = bitDepth == 16 ? (byte)(alpha >> 8) : (byte)alpha;
- }
-
- break;
- }
+ MapGrayscaleAlphaSamples(samples, width, bitDepth, destination);
+ break;
case ColorTypeTruecolorAlpha:
- {
- for (var x = 0; x < width; x++)
- {
- var s = x * 4;
- var d = x * 4;
- destination[d] = bitDepth == 16 ? (byte)(samples[s] >> 8) : (byte)samples[s];
- destination[d + 1] = bitDepth == 16 ? (byte)(samples[s + 1] >> 8) : (byte)samples[s + 1];
- destination[d + 2] = bitDepth == 16 ? (byte)(samples[s + 2] >> 8) : (byte)samples[s + 2];
- destination[d + 3] = bitDepth == 16 ? (byte)(samples[s + 3] >> 8) : (byte)samples[s + 3];
- }
-
- break;
- }
+ MapTruecolorAlphaSamples(samples, width, bitDepth, destination);
+ break;
+ }
+ }
+
+ ///
+ /// Maps one row's grayscale samples (color type 0) to RGBA, resolving the single-key-color
+ /// transparency chunk (if any) against the raw, not-yet-downshifted
+ /// sample value.
+ ///
+ ///
+ /// Isolated from as its own color-type-specific mapping
+ /// step - each PNG color type maps its samples to RGBA via an independent, self-contained
+ /// per-pixel formula, so extracting one per color type keeps every formula independently
+ /// nameable and testable rather than folding all five into one large switch body.
+ ///
+ private static void MapGrayscaleSamples(ReadOnlySpan samples, int width, int bitDepth, byte[]? trns, Span destination)
+ {
+ var maxSample = (1 << bitDepth) - 1;
+ var trnsGray = trns != null ? ReadUInt16Be(trns, 0) : -1;
+ for (var x = 0; x < width; x++)
+ {
+ var raw = samples[x];
+ var isTransparent = raw == trnsGray;
+ var gray = bitDepth == 16 ? (byte)(raw >> 8) : (byte)(raw * 255 / maxSample);
+ var d = x * 4;
+ destination[d] = gray;
+ destination[d + 1] = gray;
+ destination[d + 2] = gray;
+ destination[d + 3] = (byte)(isTransparent ? 0 : 255);
+ }
+ }
+
+ ///
+ /// Maps one row's Truecolor samples (color type 2) to RGBA, resolving the single-key-color
+ /// transparency chunk (if any) against the raw, not-yet-downshifted
+ /// RGB sample triple.
+ ///
+ ///
+ /// See 's remarks for why each color type has its own
+ /// extracted mapping method.
+ ///
+ private static void MapTruecolorSamples(ReadOnlySpan samples, int width, int bitDepth, byte[]? trns, Span destination)
+ {
+ var hasTrns = trns != null;
+ var trnsR = hasTrns ? ReadUInt16Be(trns!, 0) : -1;
+ var trnsG = hasTrns ? ReadUInt16Be(trns!, 2) : -1;
+ var trnsB = hasTrns ? ReadUInt16Be(trns!, 4) : -1;
+ for (var x = 0; x < width; x++)
+ {
+ var s = x * 3;
+ var r = samples[s];
+ var g = samples[s + 1];
+ var b = samples[s + 2];
+ var isTransparent = r == trnsR && g == trnsG && b == trnsB;
+ var d = x * 4;
+ destination[d] = bitDepth == 16 ? (byte)(r >> 8) : (byte)r;
+ destination[d + 1] = bitDepth == 16 ? (byte)(g >> 8) : (byte)g;
+ destination[d + 2] = bitDepth == 16 ? (byte)(b >> 8) : (byte)b;
+ destination[d + 3] = (byte)(isTransparent ? 0 : 255);
+ }
+ }
+
+ ///
+ /// Maps one row's palette indices (color type 3) to RGBA via ,
+ /// resolving each index's per-entry alpha from (if any).
+ ///
+ ///
+ /// See 's remarks for why each color type has its own
+ /// extracted mapping method.
+ ///
+ ///
+ /// Thrown when a sample's palette index has no corresponding entry.
+ ///
+ private static void MapPaletteSamples(ReadOnlySpan samples, int width, byte[] palette, byte[]? trns, Span destination)
+ {
+ var entries = palette.Length / 3;
+ for (var x = 0; x < width; x++)
+ {
+ var index = samples[x];
+ if (index >= entries)
+ {
+ throw new InvalidDataException(
+ $"PNG palette index {index} is out of range for a {entries}-entry PLTE chunk.");
+ }
+
+ var p = index * 3;
+ var d = x * 4;
+ destination[d] = palette[p];
+ destination[d + 1] = palette[p + 1];
+ destination[d + 2] = palette[p + 2];
+ destination[d + 3] = trns != null && index < trns.Length ? trns[index] : (byte)255;
+ }
+ }
+
+ ///
+ /// Maps one row's grayscale-with-alpha samples (color type 4) to RGBA.
+ ///
+ ///
+ /// See 's remarks for why each color type has its own
+ /// extracted mapping method.
+ ///
+ private static void MapGrayscaleAlphaSamples(ReadOnlySpan samples, int width, int bitDepth, Span destination)
+ {
+ for (var x = 0; x < width; x++)
+ {
+ var s = x * 2;
+ var gray = samples[s];
+ var alpha = samples[s + 1];
+ var d = x * 4;
+ var grayByte = bitDepth == 16 ? (byte)(gray >> 8) : (byte)gray;
+ destination[d] = grayByte;
+ destination[d + 1] = grayByte;
+ destination[d + 2] = grayByte;
+ destination[d + 3] = bitDepth == 16 ? (byte)(alpha >> 8) : (byte)alpha;
+ }
+ }
+
+ ///
+ /// Maps one row's Truecolor-with-alpha samples (color type 6) to RGBA.
+ ///
+ ///
+ /// See 's remarks for why each color type has its own
+ /// extracted mapping method.
+ ///
+ private static void MapTruecolorAlphaSamples(ReadOnlySpan samples, int width, int bitDepth, Span destination)
+ {
+ for (var x = 0; x < width; x++)
+ {
+ var s = x * 4;
+ var d = x * 4;
+ destination[d] = bitDepth == 16 ? (byte)(samples[s] >> 8) : (byte)samples[s];
+ destination[d + 1] = bitDepth == 16 ? (byte)(samples[s + 1] >> 8) : (byte)samples[s + 1];
+ destination[d + 2] = bitDepth == 16 ? (byte)(samples[s + 2] >> 8) : (byte)samples[s + 2];
+ destination[d + 3] = bitDepth == 16 ? (byte)(samples[s + 3] >> 8) : (byte)samples[s + 3];
}
}
diff --git a/src/DemaConsulting.CanvasNet/Codecs/SvgCodec.cs b/src/DemaConsulting.CanvasNet/Codecs/SvgCodec.cs
index d760d68c..15270cfa 100644
--- a/src/DemaConsulting.CanvasNet/Codecs/SvgCodec.cs
+++ b/src/DemaConsulting.CanvasNet/Codecs/SvgCodec.cs
@@ -942,7 +942,7 @@ private sealed record RenderContext(
/// codec implements no scripting/animation support that could redefine a gradient's
/// stops mid-render.
///
- public Dictionary> GradientStopCache { get; } = new();
+ public Dictionary> GradientStopCache { get; } = [];
}
// ================================================================================================
@@ -1274,7 +1274,7 @@ _ when string.Equals(dashArrayAttr.Trim(), "none", StringComparison.OrdinalIgnor
///
/// The attribute's raw, non-"none" value.
/// The parsed dash array, or for an effectively-solid stroke.
- private static IReadOnlyList? ParseDashArray(string raw)
+ private static List? ParseDashArray(string raw)
{
var numbers = ParseNumberList(raw);
return numbers.Count == 0 || numbers.Exists(v => v < 0f) || numbers.TrueForAll(v => v == 0f)
@@ -1497,7 +1497,7 @@ private static bool TryReadTransformFunction(string raw, ref int position, out M
/// Thrown when is not recognized, or
/// does not have one of the argument counts that function accepts.
///
- private static Matrix3x2 BuildTransformFunction(string name, IReadOnlyList arguments) => name switch
+ private static Matrix3x2 BuildTransformFunction(string name, List arguments) => name switch
{
"translate" => BuildTranslate(arguments),
"scale" => BuildScale(arguments),
@@ -1518,7 +1518,7 @@ private static bool TryReadTransformFunction(string raw, ref int position, out M
/// The function's parsed arguments (one or two numbers).
/// The translation matrix.
/// Thrown when the argument count is not one or two.
- private static Matrix3x2 BuildTranslate(IReadOnlyList arguments) => arguments.Count switch
+ private static Matrix3x2 BuildTranslate(List arguments) => arguments.Count switch
{
1 => Matrix3x2.CreateTranslation(arguments[0], 0f),
2 => Matrix3x2.CreateTranslation(arguments[0], arguments[1]),
@@ -1529,7 +1529,7 @@ private static bool TryReadTransformFunction(string raw, ref int position, out M
/// The function's parsed arguments (one or two numbers).
/// The scale matrix.
/// Thrown when the argument count is not one or two.
- private static Matrix3x2 BuildScale(IReadOnlyList arguments) => arguments.Count switch
+ private static Matrix3x2 BuildScale(List arguments) => arguments.Count switch
{
1 => Matrix3x2.CreateScale(arguments[0], arguments[0]),
2 => Matrix3x2.CreateScale(arguments[0], arguments[1]),
@@ -1540,7 +1540,7 @@ private static bool TryReadTransformFunction(string raw, ref int position, out M
/// The function's parsed arguments (one or three numbers, angle in degrees).
/// The rotation matrix, about the origin or about (cx, cy).
/// Thrown when the argument count is not one or three.
- private static Matrix3x2 BuildRotate(IReadOnlyList arguments)
+ private static Matrix3x2 BuildRotate(List arguments)
{
switch (arguments.Count)
{
@@ -2784,21 +2784,8 @@ private static float EstimateUniformScale(Matrix3x2 transform) =>
/// The local-space dash array, or for a solid stroke.
/// The local-to-pixel-space scale factor.
/// The scaled dash array, or if is .
- private static IReadOnlyList? ScaleDashArray(IReadOnlyList? dashArray, float scale)
- {
- if (dashArray == null)
- {
- return null;
- }
-
- var scaled = new float[dashArray.Count];
- for (var i = 0; i < dashArray.Count; i++)
- {
- scaled[i] = dashArray[i] * scale;
- }
-
- return scaled;
- }
+ private static float[]? ScaleDashArray(IReadOnlyList? dashArray, float scale) =>
+ dashArray?.Select(value => value * scale).ToArray();
// ================================================================================================
// Paint and color resolution
@@ -3403,16 +3390,8 @@ private static Rgba32 ParseStopColor(XElement stopElement)
/// The source stops.
/// The multiplier to apply to each stop's existing alpha.
/// A new list of stops with their colors' alpha scaled.
- private static List ApplyAlphaToStops(List stops, float multiplier)
- {
- var result = new List(stops.Count);
- foreach (var stop in stops)
- {
- result.Add(new GradientStop(stop.Offset, ApplyAlpha(stop.Color, multiplier)));
- }
-
- return result;
- }
+ private static List ApplyAlphaToStops(List stops, float multiplier) =>
+ stops.Select(stop => new GradientStop(stop.Offset, ApplyAlpha(stop.Color, multiplier))).ToList();
/// Parses a spreadMethod keyword.
/// The attribute's raw value, or if absent.
diff --git a/src/DemaConsulting.CanvasNet/Drawing/DashSplitter.cs b/src/DemaConsulting.CanvasNet/Drawing/DashSplitter.cs
index 25946d91..89ba1259 100644
--- a/src/DemaConsulting.CanvasNet/Drawing/DashSplitter.cs
+++ b/src/DemaConsulting.CanvasNet/Drawing/DashSplitter.cs
@@ -251,13 +251,7 @@ private static float[] NormalizeDashArray(IReadOnlyList dashArray)
{
if (dashArray.Count % 2 == 0)
{
- var even = new float[dashArray.Count];
- for (var i = 0; i < dashArray.Count; i++)
- {
- even[i] = dashArray[i];
- }
-
- return even;
+ return dashArray.ToArray();
}
var duplicated = new float[dashArray.Count * 2];
@@ -324,7 +318,7 @@ private static double GetPatternLength(IReadOnlyList pattern)
/// its while (position < totalLength) loop forever.
///
///
- private static double[] BuildCumulativeLengths(IReadOnlyList points, bool isClosed)
+ private static double[] BuildCumulativeLengths(List points, bool isClosed)
{
var edgeCount = isClosed ? points.Count : points.Count - 1;
var cumulative = new double[edgeCount + 1];
@@ -547,31 +541,7 @@ private static (int Index, float RemainingInDash) LocatePhaseBackward(
budgetExceeded = false;
var intervals = new List<(double Start, double End)>();
- // Cheap, O(pattern.Count) pre-flight estimates: short-circuit straight to the
- // cap-exceeded fallback for hopeless inputs (e.g. a huge-but-finite totalLength paired
- // with a fine dash span, or a pattern whose "on" entries dominate its transitions) without
- // ever entering the loop below - see this method's remarks for both cost models and their
- // known, bounded slack.
- var patternLength = GetPatternLength(pattern);
- var positiveEntryCount = pattern.Count(entry => entry > 0f);
- var estimatedIterations = 2d * positiveEntryCount * (totalLength / patternLength);
- if (estimatedIterations > MaxOnIntervalIterations)
- {
- budgetExceeded = true;
- return intervals;
- }
-
- var onEntryCount = 0;
- for (var i = 0; i < pattern.Count; i += 2)
- {
- if (pattern[i] > 0f)
- {
- onEntryCount++;
- }
- }
-
- var estimatedOnIntervalCount = onEntryCount * (totalLength / patternLength);
- if (estimatedOnIntervalCount > MaxOnIntervalCount)
+ if (EstimatesExceedBudget(pattern, totalLength, out var patternLength))
{
budgetExceeded = true;
return intervals;
@@ -642,6 +612,56 @@ private static (int Index, float RemainingInDash) LocatePhaseBackward(
return intervals;
}
+ ///
+ /// Cheap, O(pattern.Count) pre-flight cost estimate deciding whether
+ /// 's walking loop should even begin: short-circuits to the
+ /// cap-exceeded fallback for hopeless inputs (e.g. a huge-but-finite
+ /// paired with a fine dash span, or a pattern whose "on" entries dominate its transitions)
+ /// without ever entering the loop.
+ ///
+ ///
+ /// Isolated from as its own self-contained, independently
+ /// testable estimate - it is pure arithmetic over and
+ /// , with no dependency on the loop's own walking state.
+ /// Combines two independent estimates against two independent caps
+ /// ( and ) - see
+ /// 's own remarks for why both are necessary and for their
+ /// known, bounded slack. This is a heuristic estimate closely tracking the loop's real
+ /// cost model, not an exact prediction or a strict mathematical upper bound; the loop's
+ /// own running counters remain the authoritative backstop for any input either estimate
+ /// under-counts.
+ ///
+ /// The normalized dash pattern.
+ /// The polyline's total arc length.
+ /// Receives the pattern's total cycle length.
+ ///
+ /// if either estimate exceeds its cap and the walking loop should
+ /// be skipped entirely.
+ ///
+ private static bool EstimatesExceedBudget(IReadOnlyList pattern, double totalLength, out double patternLength)
+ {
+ patternLength = GetPatternLength(pattern);
+
+ var positiveEntryCount = pattern.Count(entry => entry > 0f);
+ var estimatedIterations = 2d * positiveEntryCount * (totalLength / patternLength);
+ if (estimatedIterations > MaxOnIntervalIterations)
+ {
+ return true;
+ }
+
+ var onEntryCount = 0;
+ for (var i = 0; i < pattern.Count; i += 2)
+ {
+ if (pattern[i] > 0f)
+ {
+ onEntryCount++;
+ }
+ }
+
+ var estimatedOnIntervalCount = onEntryCount * (totalLength / patternLength);
+ return estimatedOnIntervalCount > MaxOnIntervalCount;
+ }
+
///
/// Advances to the next positive-length dash entry.
///
@@ -670,9 +690,9 @@ private static void AdvanceDash(IReadOnlyList pattern, ref int dashIndex,
/// number of polyline edges plus the number of intervals rather than their product.
///
private static List ExtractIntervalPolyline(
- IReadOnlyList points,
+ List points,
bool isClosed,
- IReadOnlyList cumulativeLengths,
+ double[] cumulativeLengths,
double startDistance,
double endDistance,
ref int vertexCursor,
@@ -681,7 +701,7 @@ private static List ExtractIntervalPolyline(
var segment = new List();
AddPointIfDistinct(segment, GetPointAtDistance(points, isClosed, cumulativeLengths, startDistance, ref pointCursor));
- var edgeCount = cumulativeLengths.Count - 1;
+ var edgeCount = cumulativeLengths.Length - 1;
// Advance past any vertex boundaries at or before startDistance (including the implicit
// index 0 boundary, whose cumulative length is always zero).
@@ -731,9 +751,9 @@ private static List ExtractIntervalPolyline(
///
///
private static Vector2 GetPointAtDistance(
- IReadOnlyList points,
+ List points,
bool isClosed,
- IReadOnlyList cumulativeLengths,
+ double[] cumulativeLengths,
double distance,
ref int edgeCursor)
{
@@ -748,7 +768,7 @@ private static Vector2 GetPointAtDistance(
return isClosed ? points[0] : points[^1];
}
- var edgeCount = cumulativeLengths.Count - 1;
+ var edgeCount = cumulativeLengths.Length - 1;
while (edgeCursor < edgeCount - 1 && distance > cumulativeLengths[edgeCursor + 1])
{
edgeCursor++;
diff --git a/src/DemaConsulting.CanvasNet/Drawing/Gradient.cs b/src/DemaConsulting.CanvasNet/Drawing/Gradient.cs
index f5b10522..2ff79626 100644
--- a/src/DemaConsulting.CanvasNet/Drawing/Gradient.cs
+++ b/src/DemaConsulting.CanvasNet/Drawing/Gradient.cs
@@ -132,7 +132,7 @@ private static bool IsFinite(Matrix3x2 matrix) =>
///
/// Produces an immutable, stable-sorted-by-offset defensive copy of .
///
- private static IReadOnlyList CopyStableSorted(IReadOnlyList stops)
+ private static ReadOnlyCollection CopyStableSorted(IReadOnlyList stops)
{
// OrderBy is a documented-stable sort (unlike Array.Sort/List.Sort), which is required
// to preserve caller-supplied relative order among stops sharing the same Offset - see
diff --git a/src/DemaConsulting.CanvasNet/Drawing/PathFiller.cs b/src/DemaConsulting.CanvasNet/Drawing/PathFiller.cs
index 38e88446..3cb228ef 100644
--- a/src/DemaConsulting.CanvasNet/Drawing/PathFiller.cs
+++ b/src/DemaConsulting.CanvasNet/Drawing/PathFiller.cs
@@ -192,7 +192,7 @@ private static bool TryFlattenForFill(
Surface surface,
Path path,
float flattenTolerance,
- out IReadOnlyList> polygons,
+ out List> polygons,
out Rect clipBounds)
{
polygons = EdgeFlattener.Flatten(path, flattenTolerance);
diff --git a/src/DemaConsulting.CanvasNet/Drawing/PathStroker.cs b/src/DemaConsulting.CanvasNet/Drawing/PathStroker.cs
index 347ec095..7175ea11 100644
--- a/src/DemaConsulting.CanvasNet/Drawing/PathStroker.cs
+++ b/src/DemaConsulting.CanvasNet/Drawing/PathStroker.cs
@@ -1,3 +1,5 @@
+using System.Linq;
+using System.Numerics;
using DemaConsulting.CanvasNet.Geometry;
using Path = DemaConsulting.CanvasNet.Geometry.Path;
@@ -77,20 +79,8 @@ public static Path Stroke(Path path, StrokeStyle style, float flattenTolerance =
style,
flattenTolerance);
- foreach (var outline in outlines)
+ foreach (var outline in outlines.Where(outline => AppendOutlinePolygon(builder, outline)))
{
- if (outline.Count < 3)
- {
- continue;
- }
-
- builder.MoveTo(outline[0]);
- for (var i = 1; i < outline.Count; i++)
- {
- builder.LineTo(outline[i]);
- }
-
- builder.Close();
polygonCount++;
}
}
@@ -98,4 +88,33 @@ public static Path Stroke(Path path, StrokeStyle style, float flattenTolerance =
return polygonCount == 0 ? Path.Empty : builder.Build();
}
+
+ ///
+ /// Appends one stroke outliner polygon to as a closed subpath,
+ /// skipping any degenerate polygon with fewer than 3 vertices.
+ ///
+ ///
+ /// Isolated from as its own self-contained, independently testable
+ /// step - turning one outline's vertex list into path commands is a distinct concept from
+ /// the surrounding flatten/dash/outline pipeline that produces the outline.
+ ///
+ /// The path builder to append to.
+ /// The outline's vertices, in order.
+ /// if a subpath was appended; if was degenerate.
+ private static bool AppendOutlinePolygon(PathBuilder builder, IReadOnlyList outline)
+ {
+ if (outline.Count < 3)
+ {
+ return false;
+ }
+
+ builder.MoveTo(outline[0]);
+ for (var i = 1; i < outline.Count; i++)
+ {
+ builder.LineTo(outline[i]);
+ }
+
+ builder.Close();
+ return true;
+ }
}
diff --git a/src/DemaConsulting.CanvasNet/Drawing/ScanlineRasterizer.cs b/src/DemaConsulting.CanvasNet/Drawing/ScanlineRasterizer.cs
index 7ed77265..a0159442 100644
--- a/src/DemaConsulting.CanvasNet/Drawing/ScanlineRasterizer.cs
+++ b/src/DemaConsulting.CanvasNet/Drawing/ScanlineRasterizer.cs
@@ -328,47 +328,8 @@ public bool MoveNext(out int y, out ReadOnlySpan rowCoverage)
var rowTop = (float)y;
var rowBottom = rowTop + 1f;
- // Remove every edge bucketed to expire at this row (see the bucketing below):
- // this touches exactly the edges that actually expire on this row, never the
- // whole active list, so - unlike a full
- // "activeEdges.RemoveAll(edge => edge.BottomY <= rowTop)" scan of every still-
- // active edge on every row - this bookkeeping never re-examines an edge that still
- // has rows left to contribute to.
- var expiringHere = _expiringEdgeIds[y - _clipMinY];
- if (expiringHere != null)
- {
- foreach (var expiredId in expiringHere)
- {
- RemoveActiveEdge(expiredId, _activeEdges, _activeEdgeIds, _activeEdgePositions);
- }
- }
-
- while (_nextEdgeIndex < _edges.Count && _edges[_nextEdgeIndex].TopY < rowBottom)
- {
- // Each edge is identified by its own fixed position in the sorted edge table
- // ("_edges"), which never changes and is never reused, so it is a stable id to
- // bucket by even though its position within the unordered "_activeEdges" list
- // itself can move (see RemoveActiveEdge's swap-remove).
- var edgeId = _nextEdgeIndex;
- var edge = _edges[edgeId];
- _activeEdgePositions[edgeId] = _activeEdges.Count;
- _activeEdges.Add(edge);
- _activeEdgeIds.Add(edgeId);
-
- // Bucket this edge's removal at the earliest row it can actually be observed
- // as expired. Removal always happens at the *start* of a row, before this
- // row's own additions, so an edge just added this row cannot be examined for
- // expiry until at least the next row - hence the "y + 1" floor alongside the
- // edge's own BottomY.
- var expireRow = Math.Max((int)MathF.Ceiling(edge.BottomY), y + 1);
- if (expireRow < _clipMaxY)
- {
- var bucket = _expiringEdgeIds[expireRow - _clipMinY] ??= [];
- bucket.Add(edgeId);
- }
-
- _nextEdgeIndex++;
- }
+ ExpireEdgesForRow(y);
+ ActivateEdgesThroughRow(y, rowBottom);
if (_activeEdges.Count == 0)
{
@@ -381,27 +342,7 @@ public bool MoveNext(out int y, out ReadOnlySpan rowCoverage)
continue;
}
- // Accumulate every active edge's row-restricted slice into the shared cell
- // arrays - a single O(edges) pass, with no sorting and no pairing of edges into
- // "inside gaps".
- Array.Clear(_cover);
- Array.Clear(_area);
- foreach (var edge in _rowEdges)
- {
- AccumulateRowEdge(edge, _clipMinX, _clipMaxX, _cover, _area);
- }
-
- // Single left-to-right sweep: "accumulatedCover" is the running winding total. At
- // each column, "accumulatedCover" is first advanced by this column's own
- // "cover[x]", then the updated running total plus this column's own partial-edge
- // geometry ("area[x]") is resolved to a [0, 1] coverage fraction per fill rule.
- var accumulatedCover = 0f;
- for (var i = 0; i < Width; i++)
- {
- accumulatedCover += _cover[i];
- var total = accumulatedCover + _area[i];
- _rowCoverage[i] = ResolveCoverage(total, _fillRule);
- }
+ AccumulateRowCoverage();
rowCoverage = _rowCoverage;
return true;
@@ -412,6 +353,103 @@ public bool MoveNext(out int y, out ReadOnlySpan rowCoverage)
return false;
}
+ ///
+ /// Removes every active edge bucketed to expire at row (see the
+ /// bucketing performed by ): this touches exactly
+ /// the edges that actually expire on this row, never the whole active list, so -
+ /// unlike a full activeEdges.RemoveAll(edge => edge.BottomY <= rowTop)
+ /// scan of every still-active edge on every row - this bookkeeping never re-examines
+ /// an edge that still has rows left to contribute to.
+ ///
+ ///
+ /// Isolated from as its own self-contained row-sweep phase,
+ /// independently nameable from edge activation and coverage accumulation.
+ ///
+ private void ExpireEdgesForRow(int y)
+ {
+ var expiringHere = _expiringEdgeIds[y - _clipMinY];
+ if (expiringHere == null)
+ {
+ return;
+ }
+
+ foreach (var expiredId in expiringHere)
+ {
+ RemoveActiveEdge(expiredId, _activeEdges, _activeEdgeIds, _activeEdgePositions);
+ }
+ }
+
+ ///
+ /// Activates every edge in the sorted edge table whose top y falls before
+ /// , bucketing each newly-activated edge's future removal
+ /// at the earliest row it can actually be observed as expired.
+ ///
+ ///
+ /// Isolated from as its own self-contained row-sweep phase,
+ /// independently nameable from edge expiration and coverage accumulation. Removal
+ /// always happens at the start of a row, before this row's own additions, so
+ /// an edge just added this row cannot be examined for expiry until at least the next
+ /// row - hence the y + 1 floor alongside the edge's own BottomY.
+ ///
+ private void ActivateEdgesThroughRow(int y, float rowBottom)
+ {
+ while (_nextEdgeIndex < _edges.Count && _edges[_nextEdgeIndex].TopY < rowBottom)
+ {
+ // Each edge is identified by its own fixed position in the sorted edge table
+ // ("_edges"), which never changes and is never reused, so it is a stable id to
+ // bucket by even though its position within the unordered "_activeEdges" list
+ // itself can move (see RemoveActiveEdge's swap-remove).
+ var edgeId = _nextEdgeIndex;
+ var edge = _edges[edgeId];
+ _activeEdgePositions[edgeId] = _activeEdges.Count;
+ _activeEdges.Add(edge);
+ _activeEdgeIds.Add(edgeId);
+
+ var expireRow = Math.Max((int)MathF.Ceiling(edge.BottomY), y + 1);
+ if (expireRow < _clipMaxY)
+ {
+ var bucket = _expiringEdgeIds[expireRow - _clipMinY] ??= [];
+ bucket.Add(edgeId);
+ }
+
+ _nextEdgeIndex++;
+ }
+ }
+
+ ///
+ /// Accumulates every active, row-restricted edge into the shared cover/area
+ /// cell arrays, then sweeps left to right exactly once to resolve .
+ ///
+ ///
+ /// Isolated from as its own self-contained row-sweep phase,
+ /// independently nameable from edge expiration/activation - see this class's
+ /// type-level remarks for the cell-based accumulation technique.
+ ///
+ private void AccumulateRowCoverage()
+ {
+ // Accumulate every active edge's row-restricted slice into the shared cell
+ // arrays - a single O(edges) pass, with no sorting and no pairing of edges into
+ // "inside gaps".
+ Array.Clear(_cover);
+ Array.Clear(_area);
+ foreach (var edge in _rowEdges)
+ {
+ AccumulateRowEdge(edge, _clipMinX, _clipMaxX, _cover, _area);
+ }
+
+ // Single left-to-right sweep: "accumulatedCover" is the running winding total. At
+ // each column, "accumulatedCover" is first advanced by this column's own
+ // "cover[x]", then the updated running total plus this column's own partial-edge
+ // geometry ("area[x]") is resolved to a [0, 1] coverage fraction per fill rule.
+ var accumulatedCover = 0f;
+ for (var i = 0; i < Width; i++)
+ {
+ accumulatedCover += _cover[i];
+ var total = accumulatedCover + _area[i];
+ _rowCoverage[i] = ResolveCoverage(total, _fillRule);
+ }
+ }
+
///
/// Removes the active-list entry identified by (its fixed
/// index in the sorted edge table) from /
private const float NearZeroDistance = 1e-6f;
+ ///
+ /// The offsetting parameters shared by every join built for one side (
+ /// of +1/-1) of a stroked polyline: which to honor,
+ /// how far to offset (), and how finely to tessellate any round
+ /// join/cap arcs ().
+ ///
+ ///
+ /// Bundles the four arguments that , ,
+ /// and their / callers always pass
+ /// together, unchanged, for the entire side of a polyline being offset.
+ ///
+ private readonly record struct StrokeSideGeometry(float SideSign, StrokeStyle Style, float HalfWidth, float FlattenTolerance);
+
+ ///
+ /// The tessellation parameters shared by every arc-appending helper: the arc's
+ /// , the flattening bounding each
+ /// segment's sagitta, and whether the arc's start/end point should itself be emitted
+ /// (/) or left for the caller to add.
+ ///
+ ///
+ /// Bundles the four arguments , ,
+ /// and all share identically - only the arc's angular span itself
+ /// differs between the three.
+ ///
+ private readonly record struct ArcTessellation(float Radius, float FlattenTolerance, bool IncludeStart, bool IncludeEnd);
+
///
/// Converts one flattened path segment into its closed outline polygon(s).
///
@@ -72,14 +99,20 @@ internal static List> Outline(
///
private static List SimplifyPoints(IReadOnlyList points, bool isClosed)
{
- var simplified = new List(points.Count);
- foreach (var point in points)
+ // Tracks the last point that survived the filter below, so each candidate point is kept
+ // only when it differs from the previous KEPT point - not merely the previous SOURCE
+ // point - reproducing the original loop's "simplified[^1] != point" comparison exactly.
+ Vector2? lastKept = null;
+ var simplified = points.Where(point =>
{
- if (simplified.Count == 0 || simplified[^1] != point)
+ if (lastKept == point)
{
- simplified.Add(point);
+ return false;
}
- }
+
+ lastKept = point;
+ return true;
+ }).ToList();
if (isClosed && simplified.Count > 1 && simplified[0] == simplified[^1])
{
@@ -246,10 +279,7 @@ private static List> CreateOpenStrokePolygons(
leftEnd - end,
rightEnd - end,
tangent * halfWidth,
- halfWidth,
- flattenTolerance,
- includeStart: false,
- includeEnd: true);
+ new ArcTessellation(halfWidth, flattenTolerance, IncludeStart: false, IncludeEnd: true));
AppendReversed(rightSide, polygon, skipFirst: true);
}
@@ -270,10 +300,7 @@ private static List> CreateOpenStrokePolygons(
rightStart - start,
leftStart - start,
-tangent * halfWidth,
- halfWidth,
- flattenTolerance,
- includeStart: false,
- includeEnd: false);
+ new ArcTessellation(halfWidth, flattenTolerance, IncludeStart: false, IncludeEnd: false));
}
RemoveTrailingDuplicateOfFirst(polygon);
@@ -371,7 +398,26 @@ private static List> CreateClosedStrokePolygons(
// product to Infinity or (Infinity * 0) to NaN.
var outerArea = ComputeSignedArea(outerRing);
var innerArea = ComputeSignedArea(innerRing);
- if (outerArea != 0.0 && innerArea != 0.0 && Math.Sign(outerArea) == Math.Sign(innerArea))
+
+ // Areas are computed (not caller-supplied literal) values, so guard the "is this ring
+ // degenerate" check with a small tolerance rather than an exact-zero comparison - a
+ // ring that is only nearly collinear could otherwise land at a tiny nonzero double
+ // instead of exactly 0.0, and still needs to be treated as having no meaningful sign.
+ // The tolerance must scale with the rings' own area magnitude rather than use a fixed
+ // absolute cutoff: shoelace-formula rounding error grows with the coordinate magnitude,
+ // and a fixed absolute cutoff would either wrongly treat a tiny but legitimate stroke
+ // (e.g. a micron-scale closed path) as degenerate, or fail to catch genuine rounding
+ // noise on a very large one. The X and Y bounding-box spans are multiplied together
+ // (rather than combined via a single max-extent squared) so a skinny ring - large along
+ // one axis, tiny along the other - gets a tolerance matching its own area order of
+ // magnitude instead of one inflated by its unrelated long axis; each span is itself
+ // translation-invariant, so a ring far from the origin is not penalized either.
+ const double relativeAreaTolerance = 1e-9;
+ var ringAreaScale = Math.Max(RingAreaScale(outerRing), RingAreaScale(innerRing));
+ var areaNearZeroTolerance = ringAreaScale * relativeAreaTolerance;
+ if (Math.Abs(outerArea) > areaNearZeroTolerance
+ && Math.Abs(innerArea) > areaNearZeroTolerance
+ && Math.Sign(outerArea) == Math.Sign(innerArea))
{
innerRing.Reverse();
}
@@ -426,7 +472,7 @@ private static (Vector2 Tangent, Vector2 Normal)[] BuildSegmentFrames(IReadOnlyL
///
private static List BuildOpenSide(
IReadOnlyList points,
- IReadOnlyList<(Vector2 Tangent, Vector2 Normal)> frames,
+ (Vector2 Tangent, Vector2 Normal)[] frames,
float sideSign,
StrokeStyle style,
float halfWidth,
@@ -435,9 +481,10 @@ private static List BuildOpenSide(
var side = new List(points.Count * 2);
AddPointIfDistinct(side, GetOpenEndpoint(points[0], frames[0].Tangent, frames[0].Normal, sideSign, style.Cap, -1f, halfWidth));
+ var sideGeometry = new StrokeSideGeometry(sideSign, style, halfWidth, flattenTolerance);
for (var i = 1; i < points.Count - 1; i++)
{
- AppendOpenJoin(side, points[i], frames[i - 1], frames[i], sideSign, style, halfWidth, flattenTolerance);
+ AppendOpenJoin(side, points[i], frames[i - 1], frames[i], sideGeometry);
}
AddPointIfDistinct(side, GetOpenEndpoint(points[^1], frames[^1].Tangent, frames[^1].Normal, sideSign, style.Cap, +1f, halfWidth));
@@ -474,13 +521,10 @@ private static void AppendOpenJoin(
Vector2 vertex,
(Vector2 Tangent, Vector2 Normal) previousFrame,
(Vector2 Tangent, Vector2 Normal) nextFrame,
- float sideSign,
- StrokeStyle style,
- float halfWidth,
- float flattenTolerance)
+ StrokeSideGeometry geometry)
{
- var previousPoint = vertex + sideSign * previousFrame.Normal * halfWidth;
- var nextPoint = vertex + sideSign * nextFrame.Normal * halfWidth;
+ var previousPoint = vertex + geometry.SideSign * previousFrame.Normal * geometry.HalfWidth;
+ var nextPoint = vertex + geometry.SideSign * nextFrame.Normal * geometry.HalfWidth;
var turn = Cross(previousFrame.Tangent, nextFrame.Tangent);
var dot = Vector2.Dot(previousFrame.Tangent, nextFrame.Tangent);
if (MathF.Abs(turn) <= NearZeroDistance && dot > 0f)
@@ -489,7 +533,7 @@ private static void AppendOpenJoin(
return;
}
- var isConvexOnThisSide = turn * sideSign < 0f;
+ var isConvexOnThisSide = turn * geometry.SideSign < 0f;
if (!isConvexOnThisSide)
{
AddPointIfDistinct(side, previousPoint);
@@ -497,17 +541,7 @@ private static void AppendOpenJoin(
return;
}
- AppendStyledJoin(
- side,
- vertex,
- previousFrame.Tangent,
- nextFrame.Tangent,
- previousFrame.Normal,
- nextFrame.Normal,
- sideSign,
- style,
- halfWidth,
- flattenTolerance);
+ AppendStyledJoin(side, vertex, previousFrame, nextFrame, geometry);
}
///
@@ -545,6 +579,7 @@ private static List BuildClosedSide(
{
var frames = BuildSegmentFrames(points, isClosed: true);
var ring = new List(points.Count * 2);
+ var geometry = new StrokeSideGeometry(sideSign, style, halfWidth, flattenTolerance);
// Tracks, for each source vertex, the ring index of the single plain offset point emitted
// for it (see the remarks below), or -1 if that vertex instead emitted a styled join
@@ -569,14 +604,9 @@ private static List BuildClosedSide(
AppendStyledJoin(
ring,
points[i],
- previousFrame.Tangent,
- nextFrame.Tangent,
- previousFrame.Normal,
- nextFrame.Normal,
- sideSign,
- style,
- halfWidth,
- flattenTolerance,
+ previousFrame,
+ nextFrame,
+ geometry,
forceExactIntersection: !isConvexOnThisSide);
// Only a locally concave (forceExactIntersection) vertex that emitted exactly one
@@ -619,16 +649,11 @@ private static List BuildClosedSide(
///
/// Appends the requested styled join between two offset segments.
///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
+ /// The point list to append the join's vertices to.
+ /// The source polyline vertex the join is centered on.
+ /// The incoming segment's tangent/normal frame.
+ /// The outgoing segment's tangent/normal frame.
+ /// The side/style/half-width/tolerance this join is built with.
///
/// When , ignores and always emits
/// the geometrically exact intersection of the two offset edges (falling back to the
@@ -640,22 +665,17 @@ private static List BuildClosedSide(
private static void AppendStyledJoin(
List target,
Vector2 vertex,
- Vector2 previousTangent,
- Vector2 nextTangent,
- Vector2 previousNormal,
- Vector2 nextNormal,
- float sideSign,
- StrokeStyle style,
- float halfWidth,
- float flattenTolerance,
+ (Vector2 Tangent, Vector2 Normal) previousFrame,
+ (Vector2 Tangent, Vector2 Normal) nextFrame,
+ StrokeSideGeometry geometry,
bool forceExactIntersection = false)
{
- var previousPoint = vertex + sideSign * previousNormal * halfWidth;
- var nextPoint = vertex + sideSign * nextNormal * halfWidth;
+ var previousPoint = vertex + geometry.SideSign * previousFrame.Normal * geometry.HalfWidth;
+ var nextPoint = vertex + geometry.SideSign * nextFrame.Normal * geometry.HalfWidth;
if (forceExactIntersection)
{
- if (TryIntersectLines(previousPoint, previousTangent, nextPoint, nextTangent, out var intersection))
+ if (TryIntersectLines(previousPoint, previousFrame.Tangent, nextPoint, nextFrame.Tangent, out var intersection))
{
AddPointIfDistinct(target, intersection);
}
@@ -668,7 +688,7 @@ private static void AppendStyledJoin(
return;
}
- switch (style.Join)
+ switch (geometry.Style.Join)
{
case LineJoin.Round:
AddPointIfDistinct(target, previousPoint);
@@ -677,10 +697,7 @@ private static void AppendStyledJoin(
vertex,
previousPoint - vertex,
nextPoint - vertex,
- halfWidth,
- flattenTolerance,
- includeStart: false,
- includeEnd: true);
+ new ArcTessellation(geometry.HalfWidth, geometry.FlattenTolerance, IncludeStart: false, IncludeEnd: true));
break;
case LineJoin.Bevel:
@@ -689,7 +706,7 @@ private static void AppendStyledJoin(
break;
default:
- if (TryCreateMiter(vertex, previousPoint, nextPoint, previousTangent, nextTangent, style.Width, style.MiterLimit, out var miter))
+ if (TryCreateMiter(vertex, (previousPoint, previousFrame.Tangent), (nextPoint, nextFrame.Tangent), geometry.Style.Width, geometry.Style.MiterLimit, out var miter))
{
AddPointIfDistinct(target, miter);
}
@@ -719,16 +736,14 @@ private static void AppendStyledJoin(
///
private static bool TryCreateMiter(
Vector2 vertex,
- Vector2 previousPoint,
- Vector2 nextPoint,
- Vector2 previousTangent,
- Vector2 nextTangent,
+ (Vector2 Point, Vector2 Direction) previousLine,
+ (Vector2 Point, Vector2 Direction) nextLine,
float strokeWidth,
float miterLimit,
out Vector2 miterPoint)
{
miterPoint = default;
- if (!TryIntersectLines(previousPoint, previousTangent, nextPoint, nextTangent, out var intersection))
+ if (!TryIntersectLines(previousLine.Point, previousLine.Direction, nextLine.Point, nextLine.Direction, out var intersection))
{
return false;
}
@@ -775,15 +790,12 @@ private static void AppendArcShortest(
Vector2 center,
Vector2 startVector,
Vector2 endVector,
- float radius,
- float flattenTolerance,
- bool includeStart,
- bool includeEnd)
+ ArcTessellation arc)
{
var startAngle = MathF.Atan2(startVector.Y, startVector.X);
var endAngle = MathF.Atan2(endVector.Y, endVector.X);
var sweep = NormalizeSignedAngle(endAngle - startAngle);
- AppendArc(target, center, startAngle, sweep, radius, flattenTolerance, includeStart, includeEnd);
+ AppendArc(target, center, startAngle, sweep, arc);
}
///
@@ -796,10 +808,7 @@ private static void AppendArcThrough(
Vector2 startVector,
Vector2 endVector,
Vector2 throughVector,
- float radius,
- float flattenTolerance,
- bool includeStart,
- bool includeEnd)
+ ArcTessellation arc)
{
var startAngle = MathF.Atan2(startVector.Y, startVector.X);
var endAngle = MathF.Atan2(endVector.Y, endVector.X);
@@ -810,7 +819,7 @@ private static void AppendArcThrough(
sweep = sweep > 0f ? sweep - 2f * MathF.PI : sweep + 2f * MathF.PI;
}
- AppendArc(target, center, startAngle, sweep, radius, flattenTolerance, includeStart, includeEnd);
+ AppendArc(target, center, startAngle, sweep, arc);
}
///
@@ -821,26 +830,23 @@ private static void AppendArc(
Vector2 center,
float startAngle,
float sweep,
- float radius,
- float flattenTolerance,
- bool includeStart,
- bool includeEnd)
+ ArcTessellation arc)
{
- var segmentCount = GetArcSegmentCount(radius, MathF.Abs(sweep), flattenTolerance);
+ var segmentCount = GetArcSegmentCount(arc.Radius, MathF.Abs(sweep), arc.FlattenTolerance);
for (var i = 0; i <= segmentCount; i++)
{
- if (i == 0 && !includeStart)
+ if (i == 0 && !arc.IncludeStart)
{
continue;
}
- if (i == segmentCount && !includeEnd)
+ if (i == segmentCount && !arc.IncludeEnd)
{
continue;
}
var angle = startAngle + sweep * i / segmentCount;
- var point = center + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * radius;
+ var point = center + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * arc.Radius;
AddPointIfDistinct(target, point);
}
}
@@ -921,7 +927,7 @@ private static List CreateCirclePolygon(Vector2 center, float radius, f
///
/// Appends in reverse order to .
///
- private static void AppendReversed(IReadOnlyList source, List target, bool skipFirst)
+ private static void AppendReversed(List source, List target, bool skipFirst)
{
var endIndex = skipFirst ? source.Count - 2 : source.Count - 1;
for (var i = endIndex; i >= 0; i--)
@@ -1080,4 +1086,35 @@ private static double ComputeSignedArea(IReadOnlyList points)
return area / 2.0;
}
+
+ ///
+ /// Computes the product of a ring's X and Y bounding-box spans, as a proxy for its area
+ /// magnitude.
+ ///
+ ///
+ /// Used to derive an area-relative tolerance for near-zero area comparisons:
+ /// shoelace-formula rounding error grows with the coordinate magnitude, so a fixed
+ /// absolute tolerance would misclassify degeneracy at both very small and very large
+ /// scales. Multiplying the two independent axis spans (rather than squaring a single
+ /// combined extent) keeps the estimate accurate for skinny rings - large along one axis,
+ /// tiny along the other - instead of inflating the tolerance to match the longer axis.
+ /// Each span is itself translation-invariant, so a ring far from the origin is not
+ /// penalized for its absolute position either.
+ ///
+ private static double RingAreaScale(IReadOnlyList points)
+ {
+ var minX = double.PositiveInfinity;
+ var maxX = double.NegativeInfinity;
+ var minY = double.PositiveInfinity;
+ var maxY = double.NegativeInfinity;
+ foreach (var point in points)
+ {
+ minX = Math.Min(minX, point.X);
+ maxX = Math.Max(maxX, point.X);
+ minY = Math.Min(minY, point.Y);
+ maxY = Math.Max(maxY, point.Y);
+ }
+
+ return (maxX - minX) * (maxY - minY);
+ }
}
diff --git a/src/DemaConsulting.CanvasNet/Fonts/CmapTable.cs b/src/DemaConsulting.CanvasNet/Fonts/CmapTable.cs
index d49abc6e..ff274c68 100644
--- a/src/DemaConsulting.CanvasNet/Fonts/CmapTable.cs
+++ b/src/DemaConsulting.CanvasNet/Fonts/CmapTable.cs
@@ -283,43 +283,65 @@ private static int ClassifyPriority(int platformId, int encodingId, int format)
return null;
}
- return codepoint =>
+ return codepoint => LookupFormat4(codepoint, segCount, endCodes, startCodes, idDeltas, idRangeOffsets, idRangeOffsetOffset, glyphIdArrayOffset, tableEnd, data);
+ }
+
+ ///
+ /// Resolves one codepoint via a parsed format-4 subtable's segment arrays, performing the
+ /// linear segment scan and indirect glyph-index array lookup the format defines.
+ ///
+ ///
+ /// Isolated from as its own self-contained lookup step -
+ /// resolving a codepoint against already-validated segment arrays is independent of, and
+ /// independently testable from, the parsing/validation that produced those arrays.
+ ///
+ /// The resolved glyph index, or 0 (".notdef") if unmapped or malformed.
+ private static int LookupFormat4(
+ int codepoint,
+ int segCount,
+ int[] endCodes,
+ int[] startCodes,
+ short[] idDeltas,
+ ushort[] idRangeOffsets,
+ int idRangeOffsetOffset,
+ int glyphIdArrayOffset,
+ int tableEnd,
+ byte[] data)
+ {
+ if (codepoint is < 0 or > 0xFFFF)
+ {
+ return 0;
+ }
+
+ for (var i = 0; i < segCount; i++)
{
- if (codepoint is < 0 or > 0xFFFF)
+ if (codepoint > endCodes[i])
+ {
+ continue;
+ }
+
+ if (codepoint < startCodes[i])
{
return 0;
}
- for (var i = 0; i < segCount; i++)
+ if (idRangeOffsets[i] == 0)
{
- if (codepoint > endCodes[i])
- {
- continue;
- }
-
- if (codepoint < startCodes[i])
- {
- return 0;
- }
-
- if (idRangeOffsets[i] == 0)
- {
- return (codepoint + idDeltas[i]) & 0xFFFF;
- }
-
- var glyphIndexAddress = ComputeFormat4GlyphIndexAddress(
- idRangeOffsetOffset, i, idRangeOffsets[i], codepoint, startCodes[i], glyphIdArrayOffset, tableEnd);
- if (glyphIndexAddress == null)
- {
- return 0;
- }
-
- var glyphId = SfntContainer.ReadUInt16(data, (int)glyphIndexAddress.Value);
- return glyphId == 0 ? 0 : (glyphId + idDeltas[i]) & 0xFFFF;
+ return (codepoint + idDeltas[i]) & 0xFFFF;
}
- return 0;
- };
+ var glyphIndexAddress = ComputeFormat4GlyphIndexAddress(
+ idRangeOffsetOffset, i, idRangeOffsets[i], codepoint, startCodes[i], glyphIdArrayOffset, tableEnd);
+ if (glyphIndexAddress == null)
+ {
+ return 0;
+ }
+
+ var glyphId = SfntContainer.ReadUInt16(data, (int)glyphIndexAddress.Value);
+ return glyphId == 0 ? 0 : (glyphId + idDeltas[i]) & 0xFFFF;
+ }
+
+ return 0;
}
///
@@ -466,42 +488,54 @@ private static (int EndCodeOffset, int StartCodeOffset, int IdDeltaOffset, int I
}
}
- return codepoint =>
+ return codepoint => LookupFormat12(codepoint, starts, ends, startGlyphIds);
+ }
+
+ ///
+ /// Resolves one codepoint via a parsed format-12 subtable's group arrays, performing the
+ /// binary search over charCode ranges the format defines.
+ ///
+ ///
+ /// Isolated from as its own self-contained lookup step -
+ /// resolving a codepoint against already-validated group arrays is independent of, and
+ /// independently testable from, the parsing/validation that produced those arrays.
+ ///
+ /// The resolved glyph index, or 0 (".notdef") if unmapped or malformed.
+ private static int LookupFormat12(int codepoint, uint[] starts, uint[] ends, uint[] startGlyphIds)
+ {
+ if (codepoint < 0)
+ {
+ return 0;
+ }
+
+ var code = (uint)codepoint;
+ var lo = 0;
+ var hi = starts.Length - 1;
+ while (lo <= hi)
{
- if (codepoint < 0)
+ var mid = lo + (hi - lo) / 2;
+ if (code < starts[mid])
{
- return 0;
+ hi = mid - 1;
}
-
- var code = (uint)codepoint;
- var lo = 0;
- var hi = starts.Length - 1;
- while (lo <= hi)
+ else if (code > ends[mid])
+ {
+ lo = mid + 1;
+ }
+ else
{
- var mid = lo + (hi - lo) / 2;
- if (code < starts[mid])
- {
- hi = mid - 1;
- }
- else if (code > ends[mid])
- {
- lo = mid + 1;
- }
- else
- {
- // Compute the sum in `long` arithmetic rather than `uint` - `startGlyphIds[mid]`
- // combined with a large `code - starts[mid]` offset can otherwise wrap modulo
- // 2^32, silently producing a small, plausible-looking (but bogus) glyph index
- // instead of failing. Real fonts never have anywhere near `int.MaxValue`
- // glyphs, so any result that would not fit in a non-negative `int` (i.e. would
- // have wrapped, or would itself become negative when cast) is rejected
- // outright rather than truncated.
- var glyphId = (long)startGlyphIds[mid] + (code - starts[mid]);
- return glyphId > int.MaxValue ? 0 : (int)glyphId;
- }
+ // Compute the sum in `long` arithmetic rather than `uint` - `startGlyphIds[mid]`
+ // combined with a large `code - starts[mid]` offset can otherwise wrap modulo
+ // 2^32, silently producing a small, plausible-looking (but bogus) glyph index
+ // instead of failing. Real fonts never have anywhere near `int.MaxValue`
+ // glyphs, so any result that would not fit in a non-negative `int` (i.e. would
+ // have wrapped, or would itself become negative when cast) is rejected
+ // outright rather than truncated.
+ var glyphId = (long)startGlyphIds[mid] + (code - starts[mid]);
+ return glyphId > int.MaxValue ? 0 : (int)glyphId;
}
+ }
- return 0;
- };
+ return 0;
}
}
diff --git a/src/DemaConsulting.CanvasNet/Fonts/GlyfLocaReader.cs b/src/DemaConsulting.CanvasNet/Fonts/GlyfLocaReader.cs
index 13dd3d8e..51cc8386 100644
--- a/src/DemaConsulting.CanvasNet/Fonts/GlyfLocaReader.cs
+++ b/src/DemaConsulting.CanvasNet/Fonts/GlyfLocaReader.cs
@@ -79,6 +79,14 @@ internal sealed class GlyfLocaReader
private const int ScaledComponentOffset = 0x0800;
private const int UnscaledComponentOffset = 0x1000;
+ // Per-point flag bits for a simple glyph's contour data (TrueType 'glyf' simple glyph flags).
+ private const int OnCurvePoint = 0x01;
+ private const int XShortVector = 0x02;
+ private const int YShortVector = 0x04;
+ private const int RepeatFlag = 0x08;
+ private const int XIsSameOrPositiveXShortVector = 0x10;
+ private const int YIsSameOrPositiveYShortVector = 0x20;
+
private readonly byte[] _data;
private readonly int _glyfOffset;
private readonly int _glyfLength;
@@ -244,22 +252,7 @@ private Path DecodeSimpleGlyph(int glyphOffset, int glyphLength, int numberOfCon
return Path.Empty;
}
- EnsureAvailable(pos, numberOfContours * 2, limit);
- var endPts = new int[numberOfContours];
- var previousEnd = -1;
- for (var i = 0; i < numberOfContours; i++)
- {
- var value = SfntContainer.ReadUInt16(_data, pos);
- pos += 2;
- if (value <= previousEnd)
- {
- throw new InvalidDataException("Glyph contour end points must be strictly increasing.");
- }
-
- endPts[i] = value;
- previousEnd = value;
- }
-
+ var endPts = ReadContourEndPoints(numberOfContours, limit, ref pos);
var numPoints = endPts[^1] + 1;
// Charge the budget before allocating the flags/xs/ys arrays below - a crafted glyph
@@ -277,71 +270,112 @@ private Path DecodeSimpleGlyph(int glyphOffset, int glyphLength, int numberOfCon
EnsureAvailable(pos, instructionLength, limit);
pos += instructionLength;
+ var flags = ReadPointFlags(numPoints, limit, ref pos);
+ var xs = ReadCoordinates(flags, numPoints, limit, XShortVector, XIsSameOrPositiveXShortVector, ref pos);
+ var ys = ReadCoordinates(flags, numPoints, limit, YShortVector, YIsSameOrPositiveYShortVector, ref pos);
+
+ return BuildSimpleGlyphPath(numberOfContours, endPts, flags, xs, ys);
+ }
+
+ ///
+ /// Reads a simple glyph's per-contour end-point indices, validating that they are
+ /// strictly increasing (a TrueType contour is always non-empty, and contours are always
+ /// stored in point order), and advances past the bytes consumed.
+ ///
+ private int[] ReadContourEndPoints(int numberOfContours, int limit, ref int pos)
+ {
+ EnsureAvailable(pos, numberOfContours * 2, limit);
+ var endPts = new int[numberOfContours];
+ var previousEnd = -1;
+ for (var i = 0; i < numberOfContours; i++)
+ {
+ var value = SfntContainer.ReadUInt16(_data, pos);
+ pos += 2;
+ if (value <= previousEnd)
+ {
+ throw new InvalidDataException("Glyph contour end points must be strictly increasing.");
+ }
+
+ endPts[i] = value;
+ previousEnd = value;
+ }
+
+ return endPts;
+ }
+
+ ///
+ /// Decodes a simple glyph's run-length-encoded per-point flag bytes, expanding every
+ /// run into its repeated flag value, and advances
+ /// past the bytes consumed.
+ ///
+ private byte[] ReadPointFlags(int numPoints, int limit, ref int pos)
+ {
var flags = new byte[numPoints];
- var i2 = 0;
- while (i2 < numPoints)
+ var i = 0;
+ while (i < numPoints)
{
EnsureAvailable(pos, 1, limit);
var flag = _data[pos++];
- flags[i2++] = flag;
- if ((flag & 0x08) != 0)
+ flags[i++] = flag;
+ if ((flag & RepeatFlag) != 0)
{
EnsureAvailable(pos, 1, limit);
var repeatCount = _data[pos++];
- if (repeatCount > numPoints - i2)
+ if (repeatCount > numPoints - i)
{
throw new InvalidDataException("Glyph flag repeat count exceeds the declared point count.");
}
for (var r = 0; r < repeatCount; r++)
{
- flags[i2++] = flag;
+ flags[i++] = flag;
}
}
}
- var xs = new int[numPoints];
- var x = 0;
- for (var i = 0; i < numPoints; i++)
- {
- var flag = flags[i];
- if ((flag & 0x02) != 0)
- {
- EnsureAvailable(pos, 1, limit);
- var dx = _data[pos++];
- x += (flag & 0x10) != 0 ? dx : -dx;
- }
- else if ((flag & 0x10) == 0)
- {
- EnsureAvailable(pos, 2, limit);
- x += SfntContainer.ReadInt16(_data, pos);
- pos += 2;
- }
-
- xs[i] = x;
- }
+ return flags;
+ }
- var ys = new int[numPoints];
- var y = 0;
+ ///
+ /// Decodes one axis (x or y) of a simple glyph's delta-encoded, running-sum point
+ /// coordinates, and advances past the bytes consumed. Shared
+ /// between the x and y passes by taking as parameters which flag bit selects the short
+ /// (1-byte magnitude) encoding and which bit selects that byte's sign, or - for the 2-byte
+ /// encoding - whether the coordinate repeats the previous value unchanged.
+ ///
+ private int[] ReadCoordinates(byte[] flags, int numPoints, int limit, int shortVectorFlag, int sameOrPositiveFlag, ref int pos)
+ {
+ var coords = new int[numPoints];
+ var value = 0;
for (var i = 0; i < numPoints; i++)
{
var flag = flags[i];
- if ((flag & 0x04) != 0)
+ if ((flag & shortVectorFlag) != 0)
{
EnsureAvailable(pos, 1, limit);
- var dy = _data[pos++];
- y += (flag & 0x20) != 0 ? dy : -dy;
+ var delta = _data[pos++];
+ value += (flag & sameOrPositiveFlag) != 0 ? delta : -delta;
}
- else if ((flag & 0x20) == 0)
+ else if ((flag & sameOrPositiveFlag) == 0)
{
EnsureAvailable(pos, 2, limit);
- y += SfntContainer.ReadInt16(_data, pos);
+ value += SfntContainer.ReadInt16(_data, pos);
pos += 2;
}
- ys[i] = y;
+ coords[i] = value;
}
+ return coords;
+ }
+
+ ///
+ /// Assembles a simple glyph's decoded per-contour end points, flags, and x/y coordinate
+ /// arrays into a by slicing the flat point arrays back into their
+ /// per-contour ranges and emitting one call per contour.
+ ///
+ private static Path BuildSimpleGlyphPath(int numberOfContours, int[] endPts, byte[] flags, int[] xs, int[] ys)
+ {
var builder = new PathBuilder();
var contourStart = 0;
for (var c = 0; c < numberOfContours; c++)
@@ -350,7 +384,7 @@ private Path DecodeSimpleGlyph(int glyphOffset, int glyphLength, int numberOfCon
var points = new List<(Vector2 Point, bool OnCurve)>(contourEnd - contourStart + 1);
for (var p = contourStart; p <= contourEnd; p++)
{
- points.Add((new Vector2(xs[p], ys[p]), (flags[p] & 0x01) != 0));
+ points.Add((new Vector2(xs[p], ys[p]), (flags[p] & OnCurvePoint) != 0));
}
EmitContour(builder, points);
@@ -440,7 +474,6 @@ private Path DecodeCompositeGlyph(int glyphOffset, int glyphLength, int depth, r
var limit = glyphOffset + glyphLength;
var pos = glyphOffset + 10;
var builder = new PathBuilder();
- var hasContent = false;
bool more;
do
@@ -455,45 +488,8 @@ private Path DecodeCompositeGlyph(int glyphOffset, int glyphLength, int depth, r
throw new InvalidDataException("Point-matched composite glyph components are not supported.");
}
- float dx, dy;
- if ((flags & ArgsAreWords) != 0)
- {
- EnsureAvailable(pos, 4, limit);
- dx = SfntContainer.ReadInt16(_data, pos);
- dy = SfntContainer.ReadInt16(_data, pos + 2);
- pos += 4;
- }
- else
- {
- EnsureAvailable(pos, 2, limit);
- dx = unchecked((sbyte)_data[pos]);
- dy = unchecked((sbyte)_data[pos + 1]);
- pos += 2;
- }
-
- float a = 1f, b = 0f, c = 0f, d = 1f;
- if ((flags & WeHaveAScale) != 0)
- {
- EnsureAvailable(pos, 2, limit);
- a = d = SfntContainer.ReadF2Dot14(_data, pos);
- pos += 2;
- }
- else if ((flags & WeHaveAnXAndYScale) != 0)
- {
- EnsureAvailable(pos, 4, limit);
- a = SfntContainer.ReadF2Dot14(_data, pos);
- d = SfntContainer.ReadF2Dot14(_data, pos + 2);
- pos += 4;
- }
- else if ((flags & WeHaveATwoByTwo) != 0)
- {
- EnsureAvailable(pos, 8, limit);
- a = SfntContainer.ReadF2Dot14(_data, pos);
- b = SfntContainer.ReadF2Dot14(_data, pos + 2);
- c = SfntContainer.ReadF2Dot14(_data, pos + 4);
- d = SfntContainer.ReadF2Dot14(_data, pos + 6);
- pos += 8;
- }
+ var (dx, dy) = ReadComponentOffset(flags, ref pos, limit);
+ var (a, b, c, d) = ReadComponentTransform(flags, ref pos, limit);
var componentPath = DecodeGlyph(componentGlyphIndex, depth, ref totalComponents, ref totalPoints);
@@ -510,30 +506,115 @@ private Path DecodeCompositeGlyph(int glyphOffset, int glyphLength, int depth, r
dy = scaledDy;
}
- AppendTransformed(builder, componentPath, a, b, c, d, dx, dy, ref totalPoints);
- hasContent = true;
+ AppendTransformed(builder, componentPath, new Matrix3x2(a, b, c, d, dx, dy), ref totalPoints);
more = (flags & MoreComponents) != 0;
}
while (more);
- return hasContent ? builder.Build() : Path.Empty;
+ // No separate "did we add anything" tracking is needed here: the loop above always runs
+ // at least once (it is a do-while over at least one component record), but Build() already
+ // returns Path.Empty whenever no subpath was ever committed to the builder (for example
+ // when every referenced component itself resolved to Path.Empty and contributed no
+ // commands via AppendTransformed), so it produces the same result as an explicit
+ // "had any component contributed content" flag would.
+ return builder.Build();
+ }
+
+ ///
+ /// Reads a composite glyph component's translation (dx/dy) arguments,
+ /// advancing past whatever byte width the flags declare.
+ ///
+ ///
+ /// Isolated from because reading a component's offset
+ /// is a self-contained, independently testable parsing step - per the 'glyf' table spec,
+ /// alone decides whether the two argument bytes are a pair of
+ /// signed 16-bit words or a pair of signed 8-bit bytes.
+ ///
+ /// The component's flags word, previously validated to have set.
+ /// The current read position, advanced past the consumed argument bytes.
+ /// The exclusive upper bound within the read must stay within.
+ /// The component's (dx, dy) translation.
+ private (float Dx, float Dy) ReadComponentOffset(int flags, ref int pos, int limit)
+ {
+ if ((flags & ArgsAreWords) != 0)
+ {
+ EnsureAvailable(pos, 4, limit);
+ var dx = SfntContainer.ReadInt16(_data, pos);
+ var dy = SfntContainer.ReadInt16(_data, pos + 2);
+ pos += 4;
+ return (dx, dy);
+ }
+
+ EnsureAvailable(pos, 2, limit);
+ var byteDx = unchecked((sbyte)_data[pos]);
+ var byteDy = unchecked((sbyte)_data[pos + 1]);
+ pos += 2;
+ return (byteDx, byteDy);
+ }
+
+ ///
+ /// Reads a composite glyph component's optional 2x2 transform matrix
+ /// (a/b/c/d), advancing past whatever
+ /// byte width the flags declare, defaulting to the identity matrix when none of the
+ /// scale/2x2 flags are set.
+ ///
+ ///
+ /// Isolated from because parsing the transform is a
+ /// self-contained, independently testable step - per the 'glyf' table spec, exactly one of
+ /// , , or
+ /// may be set, in increasing order of generality (uniform scale, independent x/y scale, or
+ /// a full 2x2 matrix).
+ ///
+ /// The component's flags word.
+ /// The current read position, advanced past the consumed transform bytes.
+ /// The exclusive upper bound within the read must stay within.
+ /// The component's (a, b, c, d) 2x2 transform matrix.
+ private (float A, float B, float C, float D) ReadComponentTransform(int flags, ref int pos, int limit)
+ {
+ if ((flags & WeHaveAScale) != 0)
+ {
+ EnsureAvailable(pos, 2, limit);
+ var scale = SfntContainer.ReadF2Dot14(_data, pos);
+ pos += 2;
+ return (scale, 0f, 0f, scale);
+ }
+
+ if ((flags & WeHaveAnXAndYScale) != 0)
+ {
+ EnsureAvailable(pos, 4, limit);
+ var scaleX = SfntContainer.ReadF2Dot14(_data, pos);
+ var scaleY = SfntContainer.ReadF2Dot14(_data, pos + 2);
+ pos += 4;
+ return (scaleX, 0f, 0f, scaleY);
+ }
+
+ if ((flags & WeHaveATwoByTwo) != 0)
+ {
+ EnsureAvailable(pos, 8, limit);
+ var a = SfntContainer.ReadF2Dot14(_data, pos);
+ var b = SfntContainer.ReadF2Dot14(_data, pos + 2);
+ var c = SfntContainer.ReadF2Dot14(_data, pos + 4);
+ var d = SfntContainer.ReadF2Dot14(_data, pos + 6);
+ pos += 8;
+ return (a, b, c, d);
+ }
+
+ return (1f, 0f, 0f, 1f);
}
///
/// Re-issues every subpath of into ,
- /// applying the given 2x2 matrix (///)
- /// and translation (/) to every point, charging
+ /// applying the given affine (2x2 matrix plus translation, as
+ /// decoded from the composite glyph's component record) to every point, charging
/// the number of points/commands about to be copied against
/// before performing any copy - resolving a component's outline is cheap (it is already
/// built), but re-emitting it into the composite's builder is the step that actually
/// multiplies a large component's geometry by every reference to it, so the budget must be
/// checked here rather than only when the component was first decoded.
///
- private static void AppendTransformed(PathBuilder builder, Path source, float a, float b, float c, float d, float dx, float dy, ref int totalPoints)
+ private static void AppendTransformed(PathBuilder builder, Path source, Matrix3x2 transform, ref int totalPoints)
{
- Vector2 Transform(Vector2 p) => new(a * p.X + c * p.Y + dx, b * p.X + d * p.Y + dy);
-
// Count the points/commands (one MoveTo plus every command) this copy is about to
// produce, and charge the budget before copying a single one of them.
var pointsToCopy = 0;
@@ -550,17 +631,17 @@ private static void AppendTransformed(PathBuilder builder, Path source, float a,
foreach (var subpath in source.Subpaths)
{
- builder.MoveTo(Transform(subpath.Start));
+ builder.MoveTo(Vector2.Transform(subpath.Start, transform));
foreach (var command in subpath.Commands)
{
switch (command.Type)
{
case PathCommandType.LineTo:
- builder.LineTo(Transform(command.EndPoint));
+ builder.LineTo(Vector2.Transform(command.EndPoint, transform));
break;
case PathCommandType.QuadraticBezierTo:
- builder.QuadraticBezierTo(Transform(command.Control1), Transform(command.EndPoint));
+ builder.QuadraticBezierTo(Vector2.Transform(command.Control1, transform), Vector2.Transform(command.EndPoint, transform));
break;
case PathCommandType.Close:
diff --git a/src/DemaConsulting.CanvasNet/Geometry/CornerRoundEffect.cs b/src/DemaConsulting.CanvasNet/Geometry/CornerRoundEffect.cs
index b9695dd4..0fddf8cb 100644
--- a/src/DemaConsulting.CanvasNet/Geometry/CornerRoundEffect.cs
+++ b/src/DemaConsulting.CanvasNet/Geometry/CornerRoundEffect.cs
@@ -72,40 +72,9 @@ public static Path Apply(Path source, float radius)
foreach (var subpath in source.Subpaths)
{
var commands = subpath.Commands;
-
- // A "wrap-around corner" is the vertex at subpath.Start where the *implicit* closing
- // edge (drawn by a trailing Close, from the last real command's endpoint back to
- // subpath.Start) meets the *first* LineTo (from subpath.Start to commands[0].EndPoint).
- // This is the same kind of "LineTo meets LineTo" polyline corner the main loop below
- // detects for every interior vertex, except neither edge is literally adjacent in the
- // commands list - one is the implicit closing edge (whose "command" is Close, at
- // lastIndex), the other is the first command. It requires at least two LineTo
- // commands before Close (a single LineTo before Close would make the "incoming" and
- // "outgoing" edges the same segment, which is not a real corner).
var lastIndex = commands.Count - 1;
- var closingLineIndex = lastIndex - 1;
- var hasWrapAroundCorner = closingLineIndex > 0 &&
- commands[lastIndex].Type == PathCommandType.Close &&
- commands[closingLineIndex].Type == PathCommandType.LineTo &&
- commands[0].Type == PathCommandType.LineTo;
-
- var moveToPoint = subpath.Start;
- var wrapRadius = radius;
- if (hasWrapAroundCorner)
- {
- var incomingStart = commands[closingLineIndex].EndPoint;
- var outgoingEnd = commands[0].EndPoint;
- wrapRadius = ClampRadius(radius, incomingStart, subpath.Start, outgoingEnd);
-
- // Compute where the wrap-around arc's tangent-out point lands using a scratch
- // builder (the same TangentArcTo math used for every other corner), so the real
- // subpath below can start there directly instead of at the un-rounded Start.
- var scratch = new PathBuilder()
- .MoveTo(incomingStart)
- .TangentArcTo(subpath.Start, outgoingEnd, wrapRadius)
- .Build();
- moveToPoint = scratch.Subpaths[0].Commands[^1].EndPoint;
- }
+
+ var (hasWrapAroundCorner, wrapRadius, moveToPoint) = ComputeWrapAroundCorner(subpath, radius);
builder.MoveTo(moveToPoint);
@@ -198,6 +167,64 @@ public static Path Apply(Path source, float radius)
return builder.Build();
}
+ ///
+ /// Determines whether has a "wrap-around corner" at its start
+ /// point - where the implicit closing edge (drawn by a trailing ,
+ /// from the last real command's endpoint back to ) meets the
+ /// first - and, when it does, computes the rounded
+ /// radius and the tangent-out point the rest of the subpath should start from instead of
+ /// the un-rounded start point.
+ ///
+ ///
+ /// Isolated from as its own self-contained computation - determining
+ /// and rounding the wrap-around corner is independent of the traversal loop that emits the
+ /// rest of the subpath's commands, and does not depend on any state the loop accumulates.
+ /// This is the same kind of "LineTo meets LineTo" polyline corner 's
+ /// main loop detects for every interior vertex, except neither edge is literally adjacent
+ /// in the commands list - one is the implicit closing edge (whose "command" is
+ /// , at the subpath's last index), the other is the
+ /// first command. It requires at least two commands
+ /// before (a single LineTo before Close would make the
+ /// "incoming" and "outgoing" edges the same segment, which is not a real corner).
+ ///
+ /// The subpath to inspect.
+ /// The requested corner radius, before per-corner clamping.
+ ///
+ /// Whether a wrap-around corner exists, its clamped radius (equal to
+ /// when none exists), and the point the subpath should actually start from (equal to
+ /// when none exists).
+ ///
+ private static (bool HasWrapAroundCorner, float WrapRadius, Vector2 MoveToPoint) ComputeWrapAroundCorner(Subpath subpath, float radius)
+ {
+ var commands = subpath.Commands;
+ var lastIndex = commands.Count - 1;
+ var closingLineIndex = lastIndex - 1;
+ var hasWrapAroundCorner = closingLineIndex > 0 &&
+ commands[lastIndex].Type == PathCommandType.Close &&
+ commands[closingLineIndex].Type == PathCommandType.LineTo &&
+ commands[0].Type == PathCommandType.LineTo;
+
+ if (!hasWrapAroundCorner)
+ {
+ return (false, radius, subpath.Start);
+ }
+
+ var incomingStart = commands[closingLineIndex].EndPoint;
+ var outgoingEnd = commands[0].EndPoint;
+ var wrapRadius = ClampRadius(radius, incomingStart, subpath.Start, outgoingEnd);
+
+ // Compute where the wrap-around arc's tangent-out point lands using a scratch builder
+ // (the same TangentArcTo math used for every other corner), so the real subpath below
+ // can start there directly instead of at the un-rounded Start.
+ var scratch = new PathBuilder()
+ .MoveTo(incomingStart)
+ .TangentArcTo(subpath.Start, outgoingEnd, wrapRadius)
+ .Build();
+ var moveToPoint = scratch.Subpaths[0].Commands[^1].EndPoint;
+
+ return (true, wrapRadius, moveToPoint);
+ }
+
///
/// Clamps to half of the shorter of the incoming and outgoing
/// segment lengths meeting at , so an aggressive requested
diff --git a/src/DemaConsulting.CanvasNet/Geometry/Path.cs b/src/DemaConsulting.CanvasNet/Geometry/Path.cs
index f5c9d2e2..2f8effad 100644
--- a/src/DemaConsulting.CanvasNet/Geometry/Path.cs
+++ b/src/DemaConsulting.CanvasNet/Geometry/Path.cs
@@ -24,7 +24,7 @@ public sealed class Path
/// 's union-identity convention: an empty path contributes nothing to
/// any bounds it is combined with.
///
- public static readonly Path Empty = new(new List());
+ public static readonly Path Empty = new([]);
///
/// The ordered collection of independent subpaths making up this path.
@@ -116,12 +116,7 @@ private static Rect AccumulateCommandBounds(Vector2 current, PathCommand command
{
var points = new List();
BezierFlattening.FlattenQuadratic(current, command.Control1, command.EndPoint, flattenTolerance, points);
- foreach (var point in points)
- {
- bounds = bounds.Union(PointRect(point));
- }
-
- return bounds;
+ return UnionPoints(bounds, points);
}
return bounds.Union(PointRect(command.Control1)).Union(PointRect(command.EndPoint));
@@ -131,12 +126,7 @@ private static Rect AccumulateCommandBounds(Vector2 current, PathCommand command
{
var points = new List();
BezierFlattening.FlattenCubic(current, command.Control1, command.Control2, command.EndPoint, flattenTolerance, points);
- foreach (var point in points)
- {
- bounds = bounds.Union(PointRect(point));
- }
-
- return bounds;
+ return UnionPoints(bounds, points);
}
return bounds.Union(PointRect(command.Control1)).Union(PointRect(command.Control2)).Union(PointRect(command.EndPoint));
@@ -155,10 +145,7 @@ private static Rect AccumulateCommandBounds(Vector2 current, PathCommand command
{
var points = new List();
BezierFlattening.FlattenCubic(segmentStart, segment.Control1, segment.Control2, segment.End, flattenTolerance, points);
- foreach (var point in points)
- {
- bounds = bounds.Union(PointRect(point));
- }
+ bounds = UnionPoints(bounds, points);
}
else
{
@@ -176,6 +163,25 @@ private static Rect AccumulateCommandBounds(Vector2 current, PathCommand command
}
}
+ ///
+ /// Folds every point in into via
+ /// .
+ ///
+ ///
+ /// Isolated from to eliminate the identical
+ /// "flatten a curve, then union every flattened point" loop that would otherwise be
+ /// repeated for the quadratic, cubic, and arc (per-segment) flattening branches.
+ ///
+ private static Rect UnionPoints(Rect bounds, List points)
+ {
+ foreach (var point in points)
+ {
+ bounds = bounds.Union(PointRect(point));
+ }
+
+ return bounds;
+ }
+
///
/// Returns a new obtained by applying to
/// every subpath's start point and every drawing command's control points and endpoints.
diff --git a/src/DemaConsulting.CanvasNet/Geometry/PathBuilder.cs b/src/DemaConsulting.CanvasNet/Geometry/PathBuilder.cs
index d407c1fc..de5c67fd 100644
--- a/src/DemaConsulting.CanvasNet/Geometry/PathBuilder.cs
+++ b/src/DemaConsulting.CanvasNet/Geometry/PathBuilder.cs
@@ -165,7 +165,7 @@ public PathBuilder ArcTo(Vector2 radius, float rotationDegrees, bool largeArc, b
///
///
/// Degenerates gracefully: when the two rays are collinear, the incoming/outgoing
- /// vectors are zero-length, or is zero, the method emits a
+ /// vectors are near-zero-length, or is zero, the method emits a
/// single to instead of an arc.
///
///
@@ -194,7 +194,25 @@ public PathBuilder TangentArcTo(Vector2 corner, Vector2 end, float radius)
var len1 = v1.Length();
var len2 = v2.Length();
- if (radius == 0f || len1 == 0f || len2 == 0f)
+ // radius == 0f is an intentional exact-sentinel check: the caller explicitly requesting
+ // zero radius (mirroring HTML5 canvas arcTo semantics) means "no arc", regardless of how
+ // that value was produced. len1/len2, however, are Length() results of computed vectors,
+ // so a tolerance (rather than exact zero) catches a corner that is only near-coincident
+ // with start/end - avoiding an ill-conditioned near-unit-vector normalization below. Each
+ // ray gets its own tolerance, scaled only to the magnitude of the two points that define
+ // it (never the other, unrelated ray), so an ordinary long ray on the other side of the
+ // corner cannot make this ray look coincident, and a genuinely tiny local corner is not
+ // mistaken for coincidence either.
+ const float relativeCoincidenceTolerance = 1e-6f;
+ var ray1Scale = MathF.Max(
+ MathF.Max(MathF.Abs(start.X), MathF.Abs(start.Y)),
+ MathF.Max(MathF.Abs(corner.X), MathF.Abs(corner.Y)));
+ var ray2Scale = MathF.Max(
+ MathF.Max(MathF.Abs(end.X), MathF.Abs(end.Y)),
+ MathF.Max(MathF.Abs(corner.X), MathF.Abs(corner.Y)));
+ if (radius == 0f
+ || len1 <= ray1Scale * relativeCoincidenceTolerance
+ || len2 <= ray2Scale * relativeCoincidenceTolerance)
{
_currentCommands.Add(PathCommand.LineTo(corner));
return this;
diff --git a/test/DemaConsulting.CanvasNet.Tests/Canvas/SurfaceTests.cs b/test/DemaConsulting.CanvasNet.Tests/Canvas/SurfaceTests.cs
index d8c6e1fa..73fb50ee 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Canvas/SurfaceTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Canvas/SurfaceTests.cs
@@ -539,7 +539,11 @@ public void Surface_PremultiplyAlpha_MultiRowBoundaryWidths_ComputesExpectedPixe
///
private static byte ComputeExpectedPremultiplied(byte color, byte alpha)
{
- var value = Math.Round(color * alpha / 255.0, MidpointRounding.AwayFromZero);
+ // Cast the first operand to double before multiplying (rather than casting the product
+ // afterward) so the multiplication itself is performed in double precision - this is
+ // harmless for today's byte-range operands, but avoids the integer-multiplication-then-
+ // widen pattern that would silently truncate for wider integer types.
+ var value = Math.Round((double)color * alpha / 255.0, MidpointRounding.AwayFromZero);
return (byte)Math.Clamp(value, 0, 255);
}
diff --git a/test/DemaConsulting.CanvasNet.Tests/CanvasNetTests.cs b/test/DemaConsulting.CanvasNet.Tests/CanvasNetTests.cs
index 18e441c8..01813763 100644
--- a/test/DemaConsulting.CanvasNet.Tests/CanvasNetTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/CanvasNetTests.cs
@@ -318,8 +318,10 @@ public void CanvasNet_SystemIntegration_JpegGetInfoWithLargeLeadingSegments_Retu
var paddedBytes = padded.ToArray();
// Act
- var info = JpegCodec.GetInfo(new MemoryStream(paddedBytes));
- var loaded = JpegCodec.Load(new MemoryStream(paddedBytes));
+ using var infoStream = new MemoryStream(paddedBytes);
+ var info = JpegCodec.GetInfo(infoStream);
+ using var loadStream = new MemoryStream(paddedBytes);
+ var loaded = JpegCodec.Load(loadStream);
// Assert: the system reports the expected integrated declared dimensions, matching what
// Load itself decodes, without throwing merely because the probe cap was reached
@@ -361,7 +363,8 @@ public void CanvasNet_SystemIntegration_GifLoad_ReturnsExpectedPixel()
];
// Act: load the surface through the public API
- var surface = GifCodec.Load(new MemoryStream(gif));
+ using var gifStream = new MemoryStream(gif);
+ var surface = GifCodec.Load(gifStream);
// Assert: the system produces the expected integrated decoded pixel value
Assert.Equal(1, surface.Width);
@@ -675,7 +678,8 @@ public void CanvasNet_SystemIntegration_LoadFontAndFillGlyphOutline_ReturnsExpec
// Arrange: load a synthetic font, look up 'A', and extract its glyph outline in raw font
// design units (0..1000, matching this font's UnitsPerEm)
var fontData = BuildSyntheticFontWithDiamondGlyph();
- var font = TrueTypeFont.Load(new MemoryStream(fontData));
+ using var fontStream = new MemoryStream(fontData);
+ var font = TrueTypeFont.Load(fontStream);
var glyphIndex = font.GetGlyphIndex('A');
var outline = font.GetGlyphOutline(glyphIndex);
@@ -750,7 +754,8 @@ public void CanvasNet_SystemIntegration_LoadFontAndQueryMetrics_ReturnsExpectedV
// Arrange/Act: load a synthetic font and query its top-level metrics and the glyph's
// advance width through the public API
var fontData = BuildSyntheticFontWithDiamondGlyph();
- var font = TrueTypeFont.Load(new MemoryStream(fontData));
+ using var fontStream = new MemoryStream(fontData);
+ var font = TrueTypeFont.Load(fontStream);
var glyphIndex = font.GetGlyphIndex('A');
var advanceWidth = font.GetAdvanceWidth(glyphIndex);
@@ -873,7 +878,8 @@ public void CanvasNet_SystemIntegration_DrawAndMeasureTextViaCanvas_RendersAndMe
{
// Arrange: load a synthetic font with a single diamond-shaped glyph mapped from 'A'
var fontData = BuildSyntheticFontWithDiamondGlyph();
- var font = TrueTypeFont.Load(new MemoryStream(fontData));
+ using var fontStream = new MemoryStream(fontData);
+ var font = TrueTypeFont.Load(fontStream);
const float size = 32f;
var color = new Rgba32(0, 128, 255, 255);
diff --git a/test/DemaConsulting.CanvasNet.Tests/Codecs/GifCodecTests.cs b/test/DemaConsulting.CanvasNet.Tests/Codecs/GifCodecTests.cs
index 2ffada92..2af48019 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Codecs/GifCodecTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Codecs/GifCodecTests.cs
@@ -417,11 +417,13 @@ public void GifCodec_GetInfo_OversizedDimensions_ReturnsRawValue_ButLoadThrows()
WriteHeader(stream, oversized, oversized, null);
var bytes = stream.ToArray();
- var info = GifCodec.GetInfo(new MemoryStream(bytes));
+ using var infoStream = new MemoryStream(bytes);
+ var info = GifCodec.GetInfo(infoStream);
Assert.Equal(oversized, info.Width);
Assert.Equal(oversized, info.Height);
- Assert.Throws(() => GifCodec.Load(new MemoryStream(bytes)));
+ using var loadStream = new MemoryStream(bytes);
+ Assert.Throws(() => GifCodec.Load(loadStream));
}
/// Test: GifCodec_GetInfo_ZeroWidth_ThrowsInvalidDataException.
diff --git a/test/DemaConsulting.CanvasNet.Tests/Codecs/GifFixtureTests.cs b/test/DemaConsulting.CanvasNet.Tests/Codecs/GifFixtureTests.cs
index d9a40c4f..a68ab6df 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Codecs/GifFixtureTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Codecs/GifFixtureTests.cs
@@ -19,9 +19,12 @@ public class GifFixtureTests
{
///
/// The directory containing the GIF fixture corpus, copied to the test output directory
- /// by the test project's GifFixtures\** content item.
+ /// by the test project's GifFixtures\** content item. Path.Join is used
+ /// instead of purely to avoid CodeQL's
+ /// cs/path-combine rule, since Path.Join does not discard
+ /// when the second segment looks rooted.
///
- private static string AssetsPath => Path.Combine(AppContext.BaseDirectory, "GifFixtures");
+ private static string AssetsPath => Path.Join(AppContext.BaseDirectory, "GifFixtures");
///
/// Resolves a fixture file within . The file names always
diff --git a/test/DemaConsulting.CanvasNet.Tests/Codecs/JpegCodecTests.cs b/test/DemaConsulting.CanvasNet.Tests/Codecs/JpegCodecTests.cs
index 01ee2dcd..bbac63c3 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Codecs/JpegCodecTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Codecs/JpegCodecTests.cs
@@ -366,7 +366,8 @@ public void JpegCodec_Load_MissingSoiMarker_ThrowsInvalidDataException()
{
var bytes = new byte[] { 0x00, 0x00, MarkerPrefix, MarkerEoi };
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(bytes)));
+ using var ms368 = new MemoryStream(bytes);
+ Assert.Throws(() => JpegCodec.Load(ms368));
}
///
@@ -384,7 +385,8 @@ public void JpegCodec_Load_UnsupportedSofMarker_ThrowsInvalidDataException(byte
BuildSofSegment(marker, 1, 1, (1, 0x11, 0))
]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms386 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms386));
}
///
@@ -408,8 +410,10 @@ public void JpegCodec_UnsupportedSofMarkerFollowedByValidSof0_BothLoadAndGetInfo
BuildSofSegment(MarkerSof0, 4, 3, (1, 0x11, 0))
]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
- Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms410 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms410));
+ using var ms411 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.GetInfo(ms411));
}
///
@@ -430,7 +434,8 @@ public void JpegCodec_Load_FourComponentSof0_ThrowsInvalidDataException()
(4, 0x11, 0))
]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms432 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms432));
}
///
@@ -447,7 +452,8 @@ public void JpegCodec_Load_WidthExceedsMaxDimension_ThrowsInvalidDataException()
BuildSofSegment(MarkerSof0, Surface.MaxDimension + 1, 1, (1, 0x11, 0))
]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms449 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms449));
}
///
@@ -463,7 +469,8 @@ public void JpegCodec_Load_HeightExceedsMaxDimension_ThrowsInvalidDataException(
BuildSofSegment(MarkerSof0, 1, Surface.MaxDimension + 1, (1, 0x11, 0))
]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms465 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms465));
}
///
@@ -479,7 +486,8 @@ public void JpegCodec_Load_MissingSofSegment_ThrowsInvalidDataException()
BuildSosSegment((1, 0x00))
]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms481 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms481));
}
///
@@ -496,7 +504,8 @@ public void JpegCodec_Load_MissingDhtSegment_ThrowsInvalidDataException()
],
entropyData: [0x00]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms498 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms498));
}
///
@@ -513,7 +522,8 @@ public void JpegCodec_Load_MissingDqtSegment_ThrowsInvalidDataException()
],
entropyData: [0x00]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms515 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms515));
}
///
@@ -529,7 +539,8 @@ public void JpegCodec_Load_MissingSosSegment_ThrowsInvalidDataException()
BuildSofSegment(MarkerSof0, 1, 1, (1, 0x11, 0))
]);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms531 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms531));
}
///
@@ -541,7 +552,8 @@ public void JpegCodec_Load_TruncatedHeader_ThrowsInvalidDataException()
{
var jpeg = new byte[] { MarkerPrefix, MarkerSoi, MarkerPrefix, MarkerSof0, 0x00 };
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms543 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms543));
}
///
@@ -561,7 +573,8 @@ public void JpegCodec_Load_TruncatedEntropyData_ThrowsInvalidDataException()
entropyData: [],
includeEoi: false);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms563 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms563));
}
///
@@ -608,7 +621,8 @@ public static TheoryData TruncatedSegmentPayloadCases()
[MemberData(nameof(TruncatedSegmentPayloadCases))]
public void JpegCodec_Load_TruncatedSegmentPayload_ThrowsInvalidDataException(byte[] jpeg)
{
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms610 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms610));
}
// ------------------------------------------------------------------------------------------
@@ -707,8 +721,10 @@ public void JpegCodec_Load_WithRestartMarkers_DecodesIdenticallyToWithoutRestart
],
entropyData: [0x00, MarkerPrefix, MarkerRst0, 0x00]);
- var canvasWithoutRestartMarkers = JpegCodec.Load(new MemoryStream(withoutRestartMarkers));
- var canvasWithRestartMarkers = JpegCodec.Load(new MemoryStream(withRestartMarkers));
+ using var ms709 = new MemoryStream(withoutRestartMarkers);
+ var canvasWithoutRestartMarkers = JpegCodec.Load(ms709);
+ using var ms710 = new MemoryStream(withRestartMarkers);
+ var canvasWithRestartMarkers = JpegCodec.Load(ms710);
AssertPixelsApproximatelyEqual(canvasWithoutRestartMarkers, canvasWithRestartMarkers, 0);
}
@@ -777,7 +793,8 @@ public void JpegCodec_GetInfo_Grayscale_ReturnsExpectedInfoWithoutSosOrEntropyDa
BuildSofSegment(MarkerSof0, 4, 3, (1, 0x11, 0))
], includeEoi: false);
- var info = JpegCodec.GetInfo(new MemoryStream(jpeg));
+ using var ms779 = new MemoryStream(jpeg);
+ var info = JpegCodec.GetInfo(ms779);
Assert.Equal(new ImageInfo(4, 3, 1, false), info);
}
@@ -797,7 +814,8 @@ public void JpegCodec_GetInfo_Color_ReturnsExpectedInfo()
BuildSofSegment(MarkerSof0, 6, 4, (1, 0x22, 0), (2, 0x11, 1), (3, 0x11, 1))
]);
- var info = JpegCodec.GetInfo(new MemoryStream(jpeg));
+ using var ms799 = new MemoryStream(jpeg);
+ var info = JpegCodec.GetInfo(ms799);
Assert.Equal(new ImageInfo(6, 4, 3, false), info);
}
@@ -867,7 +885,8 @@ public void JpegCodec_GetInfo_MissingSoiMarker_ThrowsInvalidDataException()
{
var jpeg = new byte[] { 0x00, 0x00, 0x00, 0x00 };
- Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms869 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.GetInfo(ms869));
}
///
@@ -879,7 +898,8 @@ public void JpegCodec_GetInfo_TruncatedBeforeSofFound_ThrowsInvalidDataException
{
var jpeg = BuildJpeg([BuildMinimalDqtSegment()], includeEoi: false);
- Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms881 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.GetInfo(ms881));
}
///
@@ -891,7 +911,8 @@ public void JpegCodec_GetInfo_SosBeforeSof_ThrowsInvalidDataException()
{
var jpeg = BuildJpeg([BuildSosSegment((1, 0x00))]);
- Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms893 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.GetInfo(ms893));
}
///
@@ -911,7 +932,8 @@ public void JpegCodec_GetInfo_NoSofAnywhereEvenBeyondProbeCap_ThrowsInvalidDataE
var jpeg = BuildJpeg([], entropyData: filler, includeEoi: false);
- var exception = Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms913 = new MemoryStream(jpeg);
+ var exception = Assert.Throws(() => JpegCodec.GetInfo(ms913));
Assert.Contains("SOF0/SOF2 marker", exception.Message, StringComparison.OrdinalIgnoreCase);
}
@@ -949,7 +971,8 @@ public void JpegCodec_GetInfo_SofSegmentExtendsBeyondProbeCap_FailsForFormatReas
var jpeg = BuildJpeg([.. segments], entropyData: new byte[200_000], includeEoi: false);
- var exception = Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms951 = new MemoryStream(jpeg);
+ var exception = Assert.Throws(() => JpegCodec.GetInfo(ms951));
Assert.DoesNotContain("probe limit", exception.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("sample precision", exception.Message, StringComparison.OrdinalIgnoreCase);
}
@@ -986,8 +1009,10 @@ public void JpegCodec_GetInfo_LargeLeadingAppSegments_SucceedsAndMatchesLoadResu
var jpeg = BuildJpeg([.. segments], entropyData: [0x00, 0x00]);
// Act
- var info = JpegCodec.GetInfo(new MemoryStream(jpeg));
- var surface = JpegCodec.Load(new MemoryStream(jpeg));
+ using var ms988 = new MemoryStream(jpeg);
+ var info = JpegCodec.GetInfo(ms988);
+ using var ms989 = new MemoryStream(jpeg);
+ var surface = JpegCodec.Load(ms989);
// Assert: GetInfo succeeds (rather than throwing merely because the soft cap was
// reached) and matches Load's own decoded dimensions/channel count exactly.
@@ -1145,8 +1170,10 @@ public void JpegCodec_GetInfoVsLoad_LeadingMetadataExceedsHardLimit_BothThrowCon
var jpeg = BuildJpeg([.. segments], entropyData: [0x00, 0x00]);
// Act
- var loadException = Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
- var getInfoException = Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms1147 = new MemoryStream(jpeg);
+ var loadException = Assert.Throws(() => JpegCodec.Load(ms1147));
+ using var ms1148 = new MemoryStream(jpeg);
+ var getInfoException = Assert.Throws(() => JpegCodec.GetInfo(ms1148));
// Assert: both Load and GetInfo, on the exact same bytes, throw InvalidDataException
// referencing the hard limit - true parity, not a documented exception.
@@ -1196,8 +1223,10 @@ public void JpegCodec_GetInfoVsLoad_LeadingSegmentCountExceedsLimit_BothThrowCon
"segment-count cap can plausibly trigger the failure for either method.");
// Act
- var loadException = Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
- var getInfoException = Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms1198 = new MemoryStream(jpeg);
+ var loadException = Assert.Throws(() => JpegCodec.Load(ms1198));
+ using var ms1199 = new MemoryStream(jpeg);
+ var getInfoException = Assert.Throws(() => JpegCodec.GetInfo(ms1199));
// Assert: both Load and GetInfo, on the exact same bytes, throw InvalidDataException
// referencing the segment limit - true parity, not a documented exception.
@@ -1263,8 +1292,10 @@ public void JpegCodec_GetInfoVsLoad_SofSegmentStraddlesHardLimit_BothThrowConsis
"Test fixture must place the SOF0 segment so it straddles MaxProbeHeaderBytesHardLimit.");
// Act
- var loadException = Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
- var getInfoException = Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms1265 = new MemoryStream(jpeg);
+ var loadException = Assert.Throws(() => JpegCodec.Load(ms1265));
+ using var ms1266 = new MemoryStream(jpeg);
+ var getInfoException = Assert.Throws(() => JpegCodec.GetInfo(ms1266));
// Assert: both Load and GetInfo, on the exact same bytes, throw InvalidDataException
// referencing the hard limit even though the SOF0 segment - not merely the leading filler
@@ -1309,8 +1340,10 @@ public void JpegCodec_GetInfoVsLoad_SosBeforeSofAtSegmentCountBoundary_BothThrow
var jpeg = BuildJpeg([.. segments], entropyData: null, includeEoi: false);
// Act
- var loadException = Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
- var getInfoException = Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms1311 = new MemoryStream(jpeg);
+ var loadException = Assert.Throws(() => JpegCodec.Load(ms1311));
+ using var ms1312 = new MemoryStream(jpeg);
+ var getInfoException = Assert.Throws(() => JpegCodec.GetInfo(ms1312));
// Assert: both Load and GetInfo, on the exact same bytes, throw InvalidDataException
// referencing the SOS-before-SOF condition, not the segment-count limit - proving the SOS
@@ -1423,8 +1456,10 @@ public void JpegCodec_GetInfo_SofAsSegmentImmediatelyAfterSegmentCountLimit_Stil
var jpeg = BuildJpeg([.. segments], entropyData: [0x00, 0x00]);
// Act
- var info = JpegCodec.GetInfo(new MemoryStream(jpeg));
- var surface = JpegCodec.Load(new MemoryStream(jpeg));
+ using var ms1425 = new MemoryStream(jpeg);
+ var info = JpegCodec.GetInfo(ms1425);
+ using var ms1426 = new MemoryStream(jpeg);
+ var surface = JpegCodec.Load(ms1426);
// Assert: GetInfo succeeds (rather than throwing merely because the SOF marker landed
// immediately past the segment-count ceiling) and matches Load's own decoded
@@ -1525,7 +1560,8 @@ public void JpegCodec_GetInfo_ZeroLengthSegment_TerminatesPromptlyWithInvalidDat
// process itself would need to be killed by the CI job's own timeout (regressed to truly
// unbounded) - there is no ambiguous "slow but fine" middle ground that timing would add
// value in distinguishing.
- var caught = Assert.Throws(() => JpegCodec.GetInfo(new MemoryStream(jpeg)));
+ using var ms1527 = new MemoryStream(jpeg);
+ var caught = Assert.Throws(() => JpegCodec.GetInfo(ms1527));
// Assert: the call returned (did not hang) with the documented exception.
Assert.NotNull(caught);
@@ -1544,9 +1580,11 @@ public void JpegCodec_GetInfo_OversizedDimensions_NotRejected_ButLoadThrows()
BuildSofSegment(MarkerSof0, Surface.MaxDimension + 1, 1, (1, 0x11, 0))
]);
- var info = JpegCodec.GetInfo(new MemoryStream(jpeg));
+ using var ms1546 = new MemoryStream(jpeg);
+ var info = JpegCodec.GetInfo(ms1546);
Assert.Equal(Surface.MaxDimension + 1, info.Width);
- Assert.Throws(() => JpegCodec.Load(new MemoryStream(jpeg)));
+ using var ms1549 = new MemoryStream(jpeg);
+ Assert.Throws(() => JpegCodec.Load(ms1549));
}
}
diff --git a/test/DemaConsulting.CanvasNet.Tests/Codecs/JpegFixtureTests.cs b/test/DemaConsulting.CanvasNet.Tests/Codecs/JpegFixtureTests.cs
index 752ab918..d45b36fc 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Codecs/JpegFixtureTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Codecs/JpegFixtureTests.cs
@@ -19,15 +19,20 @@ public class JpegFixtureTests
{
///
/// The directory containing the JPEG fixture corpus, copied to the test output directory
- /// by the test project's JpegFixtures\** content item.
+ /// by the test project's JpegFixtures\** content item. Path.Join is used
+ /// instead of purely to avoid CodeQL's
+ /// cs/path-combine rule, since Path.Join does not discard
+ /// when the second segment looks rooted.
///
- private static string AssetsPath => Path.Combine(AppContext.BaseDirectory, "JpegFixtures");
+ private static string AssetsPath => Path.Join(AppContext.BaseDirectory, "JpegFixtures");
///
/// The directory containing the PngSuite corpus, copied to the test output directory by
- /// the test project's PngSuite\** content item.
+ /// the test project's PngSuite\** content item. Path.Join is used instead
+ /// of for the same CodeQL reason as
+ /// .
///
- private static string PngSuitePath => Path.Combine(AppContext.BaseDirectory, "PngSuite");
+ private static string PngSuitePath => Path.Join(AppContext.BaseDirectory, "PngSuite");
///
/// Resolves a fixture file within . The file names always
@@ -113,7 +118,7 @@ private static void AssertPixelsApproximatelyEqual(Surface expected, Surface act
[Fact]
public void JpegFixtures_SourcePngDimensions_Are32x32()
{
- var source = PngCodec.Load(Path.Combine(PngSuitePath, "basn2c08.png"));
+ var source = PngCodec.Load(ResolveFixturePath(PngSuitePath, "basn2c08.png"));
Assert.Equal(32, source.Width);
Assert.Equal(32, source.Height);
@@ -142,7 +147,7 @@ public void JpegCodec_Load_Fixture_ReturnsCanvasWithExpectedDimensions(string fi
[MemberData(nameof(ColorFixtureFiles))]
public void JpegCodec_Load_ColorFixture_MatchesSourcePngWithinTolerance(string fileName, int tolerancePerChannel)
{
- var expected = PngCodec.Load(Path.Combine(PngSuitePath, "basn2c08.png"));
+ var expected = PngCodec.Load(ResolveFixturePath(PngSuitePath, "basn2c08.png"));
var actual = JpegCodec.Load(ResolveFixturePath(AssetsPath, fileName));
AssertPixelsApproximatelyEqual(expected, actual, tolerancePerChannel);
diff --git a/test/DemaConsulting.CanvasNet.Tests/Codecs/PngSuiteTests.cs b/test/DemaConsulting.CanvasNet.Tests/Codecs/PngSuiteTests.cs
index 493c650b..375b03c6 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Codecs/PngSuiteTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Codecs/PngSuiteTests.cs
@@ -50,9 +50,12 @@ public class PngSuiteTests
{
///
/// The directory containing the PngSuite corpus, copied to the test output directory by
- /// the test project's PngSuite\** content item.
+ /// the test project's PngSuite\** content item. Path.Join is used instead
+ /// of purely to avoid CodeQL's
+ /// cs/path-combine rule, since Path.Join does not discard
+ /// when the second segment looks rooted.
///
- private static string AssetsPath => Path.Combine(AppContext.BaseDirectory, "PngSuite");
+ private static string AssetsPath => Path.Join(AppContext.BaseDirectory, "PngSuite");
///
/// Resolves a fixture file within . The file names always
diff --git a/test/DemaConsulting.CanvasNet.Tests/Codecs/SvgCodecTests.cs b/test/DemaConsulting.CanvasNet.Tests/Codecs/SvgCodecTests.cs
index 57fb0762..776cc10a 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Codecs/SvgCodecTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Codecs/SvgCodecTests.cs
@@ -50,7 +50,8 @@ private static TrueTypeFont BuildTestFont()
.AddTable("kern", kern)
.Build();
- return TrueTypeFont.Load(new MemoryStream(data));
+ using var stream = new MemoryStream(data);
+ return TrueTypeFont.Load(stream);
}
// ================================================================================================
@@ -65,7 +66,8 @@ public void SvgCodec_Load_Rect_RendersFilledRectangle()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 20, 20);
+ using var stream67 = ToStream(svg);
+ var surface = SvgCodec.Load(stream67, 20, 20);
// Assert
Assert.Equal(new Rgba32(0x11, 0x22, 0x33, 255), surface[10, 10]);
@@ -84,7 +86,8 @@ public void SvgCodec_Load_RectWithRoundedCorners_CutsCornerButFillsCenter()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream86 = ToStream(svg);
+ var surface = SvgCodec.Load(stream86, 100, 100);
// Assert: the extreme corner (well within the cut radius) is unfilled, the center is filled
Assert.Equal(0, surface[1, 1].A);
@@ -99,7 +102,8 @@ public void SvgCodec_Load_Circle_RendersFilledCircle()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream101 = ToStream(svg);
+ var surface = SvgCodec.Load(stream101, 100, 100);
// Assert: center is filled; far corner (outside the circle) is not
Assert.Equal(new Rgba32(0, 255, 0, 255), surface[50, 50]);
@@ -117,7 +121,8 @@ public void SvgCodec_Load_Ellipse_RendersElongatedFill()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream119 = ToStream(svg);
+ var surface = SvgCodec.Load(stream119, 100, 100);
// Assert: point (85,50) is within the long (x) axis's radius but would be outside a
// radius-10 circle - only a true ellipse (not a circle) fills it
@@ -137,7 +142,8 @@ public void SvgCodec_Load_Line_RendersStrokedLine()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream139 = ToStream(svg);
+ var surface = SvgCodec.Load(stream139, 100, 100);
// Assert: a point on the line is stroked; a point well away from the line is not
Assert.Equal(255, surface[50, 50].A);
@@ -157,7 +163,8 @@ public void SvgCodec_Load_Polyline_DoesNotCloseBetweenLastAndFirstPoint()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream159 = ToStream(svg);
+ var surface = SvgCodec.Load(stream159, 100, 100);
// Assert: a point on the actual "L" path is stroked
Assert.Equal(255, surface[10, 50].A);
@@ -176,7 +183,8 @@ public void SvgCodec_Load_Polygon_RendersClosedFilledShape()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream178 = ToStream(svg);
+ var surface = SvgCodec.Load(stream178, 100, 100);
// Assert: the triangle's centroid-ish interior point is filled
Assert.Equal(new Rgba32(255, 0, 255, 255), surface[50, 70]);
@@ -196,7 +204,8 @@ public void SvgCodec_Load_PathWithAbsoluteMoveLineClose_RendersFilledTriangle()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream198 = ToStream(svg);
+ var surface = SvgCodec.Load(stream198, 100, 100);
// Assert
Assert.Equal(255, surface[50, 70].A);
@@ -214,7 +223,8 @@ public void SvgCodec_Load_PathWithRelativeMoveLineClose_RendersSameShapeAsAbsolu
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream216 = ToStream(svg);
+ var surface = SvgCodec.Load(stream216, 100, 100);
// Assert: identical filled/unfilled pixels to the absolute-command test above
Assert.Equal(255, surface[50, 70].A);
@@ -232,7 +242,8 @@ public void SvgCodec_Load_PathWithHorizontalAndVerticalLines_RendersRectangle()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream234 = ToStream(svg);
+ var surface = SvgCodec.Load(stream234, 100, 100);
// Assert
Assert.Equal(255, surface[50, 50].A);
@@ -251,7 +262,8 @@ public void SvgCodec_Load_PathWithCubicBezier_RendersFilledCurvedShape()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream253 = ToStream(svg);
+ var surface = SvgCodec.Load(stream253, 100, 100);
// Assert: the base rectangle's interior is filled; well above the bulge is not
Assert.Equal(255, surface[50, 70].A);
@@ -270,7 +282,8 @@ public void SvgCodec_Load_PathWithSmoothCubicBezier_RendersFilledShape()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream272 = ToStream(svg);
+ var surface = SvgCodec.Load(stream272, 100, 100);
// Assert
Assert.Equal(255, surface[50, 60].A);
@@ -284,7 +297,8 @@ public void SvgCodec_Load_PathWithQuadraticBezier_RendersFilledShape()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream286 = ToStream(svg);
+ var surface = SvgCodec.Load(stream286, 100, 100);
// Assert
Assert.Equal(255, surface[50, 70].A);
@@ -302,7 +316,8 @@ public void SvgCodec_Load_PathWithSmoothQuadraticBezier_RendersFilledShape()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream304 = ToStream(svg);
+ var surface = SvgCodec.Load(stream304, 100, 100);
// Assert
Assert.Equal(255, surface[50, 60].A);
@@ -319,7 +334,8 @@ public void SvgCodec_Load_PathWithArc_RendersFilledHalfDisc()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream321 = ToStream(svg);
+ var surface = SvgCodec.Load(stream321, 100, 100);
// Assert: a point well within the half-disc (below the flat edge) is filled
Assert.Equal(255, surface[50, 80].A);
@@ -342,7 +358,8 @@ public void SvgCodec_Load_GroupFillInheritance_AppliesToChildWithoutOwnFill()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream344 = ToStream(svg);
+ var surface = SvgCodec.Load(stream344, 100, 100);
// Assert
Assert.Equal(new Rgba32(0xFF, 0x88, 0x00, 255), surface[25, 25]);
@@ -359,7 +376,8 @@ public void SvgCodec_Load_ChildOwnFill_OverridesGroupFill()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream361 = ToStream(svg);
+ var surface = SvgCodec.Load(stream361, 100, 100);
// Assert
Assert.Equal(new Rgba32(0, 0, 255, 255), surface[25, 25]);
@@ -376,7 +394,8 @@ public void SvgCodec_Load_NestedGroupOpacity_MultipliesIntoFillAlpha()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream378 = ToStream(svg);
+ var surface = SvgCodec.Load(stream378, 100, 100);
// Assert: resulting alpha is approximately 25% of fully opaque, not 50% or 100%
var alpha = surface[25, 25].A;
@@ -395,7 +414,8 @@ public void SvgCodec_Load_TranslateTransform_MovesShapeByOffset()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream397 = ToStream(svg);
+ var surface = SvgCodec.Load(stream397, 100, 100);
// Assert: filled at the translated position; not filled at the pre-translation position
Assert.Equal(255, surface[50, 50].A);
@@ -410,7 +430,8 @@ public void SvgCodec_Load_ScaleTransform_EnlargesShapeAboutOrigin()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream412 = ToStream(svg);
+ var surface = SvgCodec.Load(stream412, 100, 100);
// Assert: filled well within the scaled-up shape; not filled beyond it
Assert.Equal(255, surface[40, 40].A);
@@ -429,7 +450,8 @@ public void SvgCodec_Load_RotateTransform_RotatesShapeAboutOrigin()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream431 = ToStream(svg);
+ var surface = SvgCodec.Load(stream431, 100, 100);
// Assert: the square (originally centered near local (40,0)) now renders near (0,40)
Assert.Equal(255, surface[0, 40].A);
@@ -445,7 +467,8 @@ public void SvgCodec_Load_MatrixTransform_AppliesRawComponents()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream447 = ToStream(svg);
+ var surface = SvgCodec.Load(stream447, 100, 100);
// Assert
Assert.Equal(255, surface[50, 50].A);
@@ -464,7 +487,8 @@ public void SvgCodec_Load_SkewXTransform_ShearsShapeAlongX()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream466 = ToStream(svg);
+ var surface = SvgCodec.Load(stream466, 100, 100);
// Assert: near the top (y=1), the strip has barely moved and still covers x~13
Assert.Equal(255, surface[13, 1].A);
@@ -490,7 +514,8 @@ public void SvgCodec_Load_CombinedTransformFunctions_AppliesRightmostFunctionFir
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream492 = ToStream(svg);
+ var surface = SvgCodec.Load(stream492, 100, 100);
// Assert: filled at the "rotate-then-translate" expected position
Assert.Equal(255, surface[40, 80].A);
@@ -514,7 +539,8 @@ public void SvgCodec_Load_FillRuleEvenOdd_PunchesHoleInOverlappingSubpaths()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream516 = ToStream(svg);
+ var surface = SvgCodec.Load(stream516, 100, 100);
// Assert: the shared overlapping center is a hole (unfilled); the outer ring is filled
Assert.Equal(0, surface[50, 50].A);
@@ -532,7 +558,8 @@ public void SvgCodec_Load_FillRuleNonzeroDefault_FillsUnionOfOverlappingSubpaths
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream534 = ToStream(svg);
+ var surface = SvgCodec.Load(stream534, 100, 100);
// Assert: the center is filled under nonzero, unlike under evenodd
Assert.Equal(255, surface[50, 50].A);
@@ -549,7 +576,8 @@ public void SvgCodec_Load_FillNoneWithStroke_RendersOnlyOutline()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream551 = ToStream(svg);
+ var surface = SvgCodec.Load(stream551, 100, 100);
// Assert: the outline (near x=20) is stroked; the interior (center) is not filled
Assert.Equal(255, surface[20, 50].A);
@@ -567,7 +595,8 @@ public void SvgCodec_Load_StrokeDasharray_RendersGapsAlongLine()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream569 = ToStream(svg);
+ var surface = SvgCodec.Load(stream569, 100, 100);
// Assert: at least one sampled point along the line is unstroked (a dash gap) - a solid
// stroke (dasharray ignored) would leave every sampled point stroked
@@ -604,7 +633,8 @@ public void SvgCodec_Load_StrokeMiterLimitZero_FallsBackToInheritedDefaultWithou
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream606 = ToStream(svg);
+ var surface = SvgCodec.Load(stream606, 100, 100);
// Assert: the outline still renders (no exception, and the stroke was not dropped)
Assert.Equal(255, surface[20, 50].A);
@@ -622,7 +652,8 @@ public void SvgCodec_Load_StrokeMiterLimitNegative_FallsBackToInheritedDefaultWi
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream624 = ToStream(svg);
+ var surface = SvgCodec.Load(stream624, 100, 100);
// Assert: the outline still renders (no exception, and the stroke was not dropped)
Assert.Equal(255, surface[20, 50].A);
@@ -645,7 +676,8 @@ public void SvgCodec_Load_StrokeMiterLimitNaN_FallsBackToInheritedDefaultWithout
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream647 = ToStream(svg);
+ var surface = SvgCodec.Load(stream647, 100, 100);
// Assert: the outline still renders (no exception, and the stroke was not dropped)
Assert.Equal(255, surface[20, 50].A);
@@ -668,7 +700,8 @@ public void SvgCodec_Load_StrokeMiterLimitInfinity_FallsBackToInheritedDefaultWi
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream670 = ToStream(svg);
+ var surface = SvgCodec.Load(stream670, 100, 100);
// Assert: the outline still renders (no exception, and the stroke was not dropped)
Assert.Equal(255, surface[20, 50].A);
@@ -685,7 +718,8 @@ public void SvgCodec_Load_StrokeMiterLimitValid_RendersNormally()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream687 = ToStream(svg);
+ var surface = SvgCodec.Load(stream687, 100, 100);
// Assert: the outline renders normally
Assert.Equal(255, surface[20, 50].A);
@@ -741,7 +775,8 @@ public void SvgCodec_Load_ExtremeMiterLimitWithSpikeVertexSynthesizesOversizedMi
"";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream743 = ToStream(svg);
+ var surface = SvgCodec.Load(stream743, 100, 100);
// Assert: rendering completed without incident (no hang/crash), and the whole stroke -
// which, un-skipped, would have engulfed the entire 100x100 canvas given its 900,000-unit
@@ -771,7 +806,8 @@ public void SvgCodec_Load_NormalMiterJoinWithDefaultMiterLimit_RendersNormally()
"";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream773 = ToStream(svg);
+ var surface = SvgCodec.Load(stream773, 100, 100);
// Assert: the corner's sharp miter tip renders as expected, near (60,20)
Assert.Equal(255, surface[60, 20].A);
@@ -821,7 +857,8 @@ public void SvgCodec_Load_NestedTransformScaleOverflowsStrokeWidthToInfinity_Ski
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream823 = ToStream(svg);
+ var surface = SvgCodec.Load(stream823, 100, 100);
// Assert: rendering completed without the raw ArgumentOutOfRangeException a non-finite
// effective stroke width reaching StrokeStyle's constructor would otherwise throw
@@ -863,7 +900,8 @@ public void SvgCodec_Load_StrokeWidthScaledPastMagnitudeBound_SkipsStrokeWithout
"";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream865 = ToStream(svg);
+ var surface = SvgCodec.Load(stream865, 100, 100);
// Assert: the fill still renders normally (its own transformed geometry stays within
// bound) ...
@@ -917,7 +955,8 @@ public void SvgCodec_Load_NestedTransformScaleOverflowsGradientTransformToNonFin
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream919 = ToStream(svg);
+ var surface = SvgCodec.Load(stream919, 100, 100);
// Assert: loads without throwing, and the (skipped) rect leaves nothing rendered
Assert.Equal(100, surface.Width);
@@ -967,7 +1006,8 @@ public void SvgCodec_Load_ScaledDashArrayOverflowsToInfinity_FallsBackToSolidStr
// Act & Assert: rejected at parse time, well before RenderStroke's own scaling would ever
// run
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream969 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream969, 100, 100));
}
///
@@ -981,7 +1021,8 @@ public void SvgCodec_Load_Opacity_MultipliesIntoFillAlpha()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream983 = ToStream(svg);
+ var surface = SvgCodec.Load(stream983, 100, 100);
// Assert: alpha is approximately 40% of fully opaque (0.4 * 255 = 102), not 0 or 255
Assert.InRange((int)surface[25, 25].A, 90, 112);
@@ -1013,7 +1054,8 @@ public void SvgCodec_Load_LinearGradientUserSpaceOnUse_VariesAlongUserSpaceAxis(
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1015 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1015, 100, 100);
// Assert: brightness increases left to right, and reflects the user-space (not
// bounding-box-relative) axis
@@ -1041,7 +1083,8 @@ public void SvgCodec_Load_RadialGradient_VariesFromCenterToEdge()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1043 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1043, 100, 100);
// Assert: the center is brighter than a point near the shape's edge
Assert.True(surface[50, 50].R > surface[95, 50].R);
@@ -1074,7 +1117,8 @@ public void SvgCodec_Load_RadialGradientRNegative_FallsBackToDefaultRadiusWithou
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1076 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1076, 100, 100);
// Assert: the rect still renders (no exception, and the fill was not dropped)
Assert.Equal(255, surface[5, 5].A);
@@ -1102,7 +1146,8 @@ public void SvgCodec_Load_RadialGradientFrNegative_FallsBackToDefaultFocalRadius
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1104 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1104, 100, 100);
// Assert: the rect still renders (no exception, and the fill was not dropped)
Assert.Equal(255, surface[5, 5].A);
@@ -1129,7 +1174,8 @@ public void SvgCodec_Load_RadialGradientRFrValid_RendersNormally()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1131 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1131, 100, 100);
// Assert: the center is brighter than a point near the shape's edge (gradient still applied)
Assert.True(surface[50, 50].R > surface[95, 50].R);
@@ -1184,7 +1230,8 @@ public void SvgCodec_Load_GradientTransformComposedWithHugeBoundingBoxOverflowsT
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1186 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1186, 100, 100);
// Assert: loads without throwing, and the gradient fill is tolerated as "no paint"
Assert.Equal(100, surface.Width);
@@ -1214,7 +1261,8 @@ public void SvgCodec_Load_GradientSpreadMethodRepeat_TilesPastBaseRange()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1216 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1216, 100, 100);
// Assert: the tiled positions render nearly the same color (repeat), and that color is
// not the fully-clamped white a "pad" (default) spread would produce at x equals 25
@@ -1243,7 +1291,8 @@ public void SvgCodec_Load_StrokeGradient_PaintsVaryingColorAlongStroke()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1245 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1245, 100, 100);
// Assert: the stroke is darker near the start than near the end
Assert.True(surface[10, 50].R < surface[90, 50].R);
@@ -1271,7 +1320,8 @@ public void SvgCodec_Load_GradientHrefInheritance_InheritsStopsFromTemplate()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1273 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1273, 100, 100);
// Assert: the inherited stops still produce a left-to-right brightness gradient
Assert.True(surface[10, 50].R < surface[90, 50].R);
@@ -1300,7 +1350,8 @@ public void SvgCodec_Load_GradientHrefChainOfTwoHops_ResolvesToEventualStops()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1302 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1302, 100, 100);
// Assert
Assert.True(surface[10, 50].R < surface[90, 50].R);
@@ -1325,7 +1376,8 @@ public void SvgCodec_Load_GradientHrefCycle_ThrowsInvalidDataException()
""";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream1327 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream1327, 100, 100));
}
///
@@ -1357,7 +1409,8 @@ public void SvgCodec_Load_GradientReferencedByManyShapes_CachesStopsAndRendersId
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 50);
+ using var stream1359 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1359, 100, 50);
// Assert: every one of the 50 rows shows the same left-to-right brightness ramp from the
// shared, cached gradient
@@ -1441,7 +1494,8 @@ public void SvgCodec_Load_UseElement_RendersCopyAtOffsetIndependentOfDocumentOrd
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 10, 10);
+ using var stream1443 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1443, 10, 10);
// Assert: the original box renders at (0,0)-(4,4)
Assert.Equal(255, surface[1, 1].A);
@@ -1460,7 +1514,8 @@ public void SvgCodec_Load_UseElementDanglingReference_IsSilentNoOp()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 10, 10);
+ using var stream1462 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1462, 10, 10);
// Assert: no exception, and nothing was rendered
Assert.Equal(0, surface[5, 5].A);
@@ -1487,7 +1542,8 @@ public void SvgCodec_Load_UseElementReferencingGroup_RendersAllGroupChildren()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 20, 20);
+ using var stream1489 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1489, 20, 20);
// Assert: both group children rendered
Assert.Equal(255, surface[1, 1].A);
@@ -1512,7 +1568,8 @@ public void SvgCodec_Load_UseElementMutualRecursionCycle_ThrowsInvalidDataExcept
""";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream1514 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream1514, 10, 10));
}
///
@@ -1552,7 +1609,8 @@ public void SvgCodec_Load_UseFanOutExceedingTotalElementBudget_ThrowsInvalidData
builder.Append("");
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(builder.ToString()), 10, 10));
+ using var stream1554 = ToStream(builder.ToString());
+ Assert.Throws(() => SvgCodec.Load(stream1554, 10, 10));
}
///
@@ -1574,7 +1632,8 @@ public void SvgCodec_Load_DeeplyNestedGroups_ThrowsInvalidDataException()
+ "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream1576 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream1576, 10, 10));
}
// ================================================================================================
@@ -1600,7 +1659,8 @@ public void SvgCodec_Load_PathDataExceedingGeometryWorkBudget_ThrowsInvalidDataE
var svg = $"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream1602 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream1602, 10, 10));
}
///
@@ -1618,7 +1678,8 @@ public void SvgCodec_Load_PointListExceedingGeometryWorkBudget_ThrowsInvalidData
var svg = $"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream1620 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream1620, 10, 10));
}
///
@@ -1648,7 +1709,7 @@ public void SvgCodec_Load_PointsListLargeExceedingBudget_ThrowsWithoutLargeAlloc
const int hugePairCount = 1_000_000;
var points = string.Concat(Enumerable.Repeat("1,1 ", hugePairCount));
var svg = $"";
- var stream = ToStream(svg);
+ using var stream = ToStream(svg);
// Act
var allocatedBefore = GC.GetAllocatedBytesForCurrentThread();
@@ -1681,7 +1742,8 @@ public void SvgCodec_Load_TextExceedingGeometryWorkBudget_ThrowsInvalidDataExcep
var fonts = new Dictionary { ["TestFont"] = BuildTestFont() };
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10, fonts));
+ using var stream1683 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream1683, 10, 10, fonts));
}
// ================================================================================================
@@ -1705,7 +1767,7 @@ public void SvgCodec_Load_StrokeDasharrayExceedingNumberListLengthCap_ThrowsWith
const int hugeNumberCount = 1_000_000;
var dasharray = string.Join(',', Enumerable.Repeat("1", hugeNumberCount));
var svg = $"";
- var stream = ToStream(svg);
+ using var stream = ToStream(svg);
// Act
var allocatedBefore = GC.GetAllocatedBytesForCurrentThread();
@@ -1734,7 +1796,8 @@ public void SvgCodec_Load_TransformArgumentListExceedingLengthCap_ThrowsInvalidD
var svg = $"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream1736 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream1736, 10, 10));
}
///
@@ -1752,7 +1815,8 @@ public void SvgCodec_Load_ViewBoxNumberListExceedingLengthCap_ThrowsInvalidDataE
var svg = $"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream1754 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream1754, 10, 10));
}
// ================================================================================================
@@ -1783,7 +1847,8 @@ public void SvgCodec_Load_UnrenderedDefsElementCountExceedingDocumentElementBudg
builder.Append("");
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(builder.ToString()), 10, 10));
+ using var stream1785 = ToStream(builder.ToString());
+ Assert.Throws(() => SvgCodec.Load(stream1785, 10, 10));
}
///
@@ -1810,7 +1875,7 @@ public void SvgCodec_Load_GradientStopCountExceedingDocumentElementBudget_Throws
builder.Append("");
builder.Append("");
- var stream = ToStream(builder.ToString());
+ using var stream = ToStream(builder.ToString());
// Act
var allocatedBefore = GC.GetAllocatedBytesForCurrentThread();
@@ -1843,7 +1908,8 @@ public void SvgCodec_Load_TextWithMatchingFont_RendersGlyphAtExpectedPosition()
var fonts = new Dictionary { ["TestFont"] = BuildTestFont() };
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100, fonts);
+ using var stream1845 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1845, 100, 100, fonts);
// Assert: inside the glyph square
Assert.Equal(255, surface[35, 35].A);
@@ -1866,7 +1932,8 @@ public void SvgCodec_Load_TextAnchorMiddle_CentersTextHorizontally()
var fonts = new Dictionary { ["TestFont"] = BuildTestFont() };
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100, fonts);
+ using var stream1868 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1868, 100, 100, fonts);
// Assert: filled within the centered square
Assert.Equal(255, surface[25, 35].A);
@@ -1887,7 +1954,8 @@ public void SvgCodec_Load_TextAnchorEnd_RightAlignsText()
var fonts = new Dictionary { ["TestFont"] = BuildTestFont() };
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100, fonts);
+ using var stream1889 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1889, 100, 100, fonts);
// Assert: filled within the visible part of the right-aligned square
Assert.Equal(255, surface[20, 35].A);
@@ -1910,7 +1978,8 @@ public void SvgCodec_Load_TextWithKerningPair_AppliesKerningBetweenGlyphs()
var fonts = new Dictionary { ["TestFont"] = BuildTestFont() };
// Act
- var surface = SvgCodec.Load(ToStream(svg), 200, 100, fonts);
+ using var stream1912 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1912, 200, 100, fonts);
// Assert: filled only if the -10 kerning adjustment was applied before placing 'B'
Assert.Equal(255, surface[105, 35].A);
@@ -1930,7 +1999,8 @@ public void SvgCodec_Load_TextWithoutFontsDictionary_SkipsSilentlyWithoutThrowin
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1932 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1932, 100, 100);
// Assert: no exception was thrown (implicit), and no ink was painted anywhere
Assert.Equal(0, surface[35, 35].A);
@@ -1948,7 +2018,8 @@ public void SvgCodec_Load_TextFontFamilyNoMatch_SkipsSilentlyWithoutThrowing()
var fonts = new Dictionary { ["OtherFont"] = BuildTestFont() };
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100, fonts);
+ using var stream1950 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1950, 100, 100, fonts);
// Assert: no exception, and no ink was painted
Assert.Equal(0, surface[35, 35].A);
@@ -1966,7 +2037,8 @@ public void SvgCodec_Load_TextFontFamilyCommaSeparatedList_MatchesFirstAvailable
var fonts = new Dictionary { ["TestFont"] = BuildTestFont() };
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100, fonts);
+ using var stream1968 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1968, 100, 100, fonts);
// Assert: the glyph rendered, proving the fallback list was walked to "TestFont"
Assert.Equal(255, surface[35, 35].A);
@@ -1988,7 +2060,8 @@ public void SvgCodec_Load_WideViewBoxIntoSquareRaster_LetterboxesTopAndBottom()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream1990 = ToStream(svg);
+ var surface = SvgCodec.Load(stream1990, 100, 100);
// Assert: content band is filled
Assert.Equal(255, surface[50, 50].A);
@@ -2009,7 +2082,8 @@ public void SvgCodec_Load_TallViewBoxIntoSquareRaster_LetterboxesLeftAndRight()
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream2011 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2011, 100, 100);
// Assert: content band is filled
Assert.Equal(255, surface[50, 50].A);
@@ -2034,7 +2108,8 @@ public void SvgCodec_GetInfo_ViewBoxPresent_ReturnsViewBoxDimensions()
const string svg = "";
// Act
- var info = SvgCodec.GetInfo(ToStream(svg));
+ using var stream2036 = ToStream(svg);
+ var info = SvgCodec.GetInfo(stream2036);
// Assert
Assert.Equal(40, info.Width);
@@ -2054,7 +2129,8 @@ public void SvgCodec_GetInfo_NoViewBoxWidthHeightPresent_ReturnsWidthHeight()
const string svg = "";
// Act
- var info = SvgCodec.GetInfo(ToStream(svg));
+ using var stream2056 = ToStream(svg);
+ var info = SvgCodec.GetInfo(stream2056);
// Assert
Assert.Equal(64, info.Width);
@@ -2073,7 +2149,8 @@ public void SvgCodec_GetInfo_NoViewBoxNoWidthHeight_ReturnsCssDefault300x150()
const string svg = "";
// Act
- var info = SvgCodec.GetInfo(ToStream(svg));
+ using var stream2075 = ToStream(svg);
+ var info = SvgCodec.GetInfo(stream2075);
// Assert
Assert.Equal(300, info.Width);
@@ -2135,7 +2212,8 @@ public void SvgCodec_GetInfo_ViewBoxWidthExceedsInt32Range_ClampsToInt32MaxValue
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.GetInfo(ToStream(svg)));
+ using var stream2137 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.GetInfo(stream2137));
}
///
@@ -2150,7 +2228,8 @@ public void SvgCodec_GetInfo_WidthExceedsInt32Range_ClampsToInt32MaxValueWithout
const string svg = "";
// Act
- var info = SvgCodec.GetInfo(ToStream(svg));
+ using var stream2152 = ToStream(svg);
+ var info = SvgCodec.GetInfo(stream2152);
// Assert
Assert.Equal(int.MaxValue, info.Width);
@@ -2173,7 +2252,8 @@ public void SvgCodec_GetInfo_MalformedXmlAfterRootElement_DoesNotThrowAndReturns
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.GetInfo(ToStream(svg)));
+ using var stream2203 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.GetInfo(stream2203));
}
// ================================================================================================
@@ -2225,7 +2306,8 @@ public void SvgCodec_Load_DocumentWithDoctypeDeclaration_ThrowsInvalidDataExcept
const string svg = "]>";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2227 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2227, 100, 100));
}
///
@@ -2246,7 +2328,8 @@ public void SvgCodec_Load_DocumentWithExternalEntityDoctype_ThrowsInvalidDataExc
"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2248 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2248, 100, 100));
}
///
@@ -2265,7 +2348,8 @@ public void SvgCodec_GetInfo_DocumentWithExternalEntityDoctype_ThrowsInvalidData
"";
// Act & Assert
- Assert.Throws(() => SvgCodec.GetInfo(ToStream(svg)));
+ using var stream2267 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.GetInfo(stream2267));
}
// ================================================================================================
@@ -2283,7 +2367,8 @@ public void SvgCodec_Load_MalformedXml_ThrowsInvalidDataException()
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream2395 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2395, 10, 10));
}
///
@@ -2422,7 +2513,8 @@ public void SvgCodec_Load_HugeFinitePathWithFineDashPattern_TerminatesPromptlyWi
"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream2424 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2424, 10, 10));
}
///
@@ -2454,7 +2546,8 @@ public void SvgCodec_Load_PathSmoothCubicReflectionOverflowsToInfinity_SkipsPath
"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream2456 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2456, 10, 10));
}
///
@@ -2489,7 +2582,8 @@ public void SvgCodec_Load_PathArcCommandRadiusOverflowsToNonFinite_SkipsPathWith
"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream2491 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2491, 10, 10));
}
///
@@ -2517,7 +2611,8 @@ public void SvgCodec_Load_RectRoundedCornerArcConversionOverflowsToNonFinite_Ski
"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 10, 10));
+ using var stream2519 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2519, 10, 10));
}
///
@@ -2559,7 +2654,8 @@ public void SvgCodec_Load_TransformScaleAmplifiesCoordinatePastMagnitudeBound_Sk
"";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream2561 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2561, 100, 100);
// Assert: Load did not throw, and the shape was tolerantly skipped entirely - the sampled
// pixel, which the scaled square's huge bounding box would otherwise cover if the shape
@@ -2578,7 +2674,8 @@ public void SvgCodec_Load_MalformedTransformUnrecognizedFunction_ThrowsInvalidDa
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2580 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2580, 100, 100));
}
///
@@ -2599,7 +2696,8 @@ public void SvgCodec_Load_DocumentExceedingCharacterBudget_ThrowsInvalidDataExce
var svg = $"";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2601 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2601, 100, 100));
}
///
@@ -2616,7 +2714,8 @@ public void SvgCodec_Load_DocumentWithinCharacterBudget_LoadsSuccessfully()
var svg = $"";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 10, 10);
+ using var stream2618 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2618, 10, 10);
// Assert: the rect still rendered
Assert.Equal(255, surface[5, 5].A);
@@ -2637,7 +2736,8 @@ public void SvgCodec_Load_WidthAttributeNaN_ThrowsInvalidDataException()
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2639 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2639, 100, 100));
}
///
@@ -2653,7 +2753,8 @@ public void SvgCodec_Load_StrokeWidthInfinity_ThrowsInvalidDataException()
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2655 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2655, 100, 100));
}
///
@@ -2674,7 +2775,8 @@ public void SvgCodec_Load_PathDataNumberOverflowToInfinity_ThrowsInvalidDataExce
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2676 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2676, 100, 100));
}
///
@@ -2691,7 +2793,8 @@ public void SvgCodec_Load_PointsListNumberOverflowToInfinity_ThrowsInvalidDataEx
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2693 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2693, 100, 100));
}
///
@@ -2708,7 +2811,8 @@ public void SvgCodec_Load_PathDataCoordinateExceedingMaxMagnitude_ThrowsInvalidD
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2710 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2710, 100, 100));
}
///
@@ -2723,7 +2827,8 @@ public void SvgCodec_Load_PointsListCoordinateExceedingMaxMagnitude_ThrowsInvali
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2725 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2725, 100, 100));
}
///
@@ -2746,7 +2851,8 @@ public void SvgCodec_Load_CoordinateWithinMaxMagnitude_RendersSuccessfully()
"";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 10, 10);
+ using var stream2748 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2748, 10, 10);
// Assert: no exception, and the rect (which covers the whole viewBox) rendered
Assert.Equal(255, surface[5, 5].A);
@@ -2765,7 +2871,8 @@ public void SvgCodec_Load_NegativeScientificAndPercentageValues_RendersWithoutTh
const string svg = "";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream2767 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2767, 100, 100);
// Assert: the rect (x=-10, width=100 => spans to x=90) covers (50,25) at ~50% opacity
// (0.5 * 255 = 127.5 ~ 127/128), not 0 (rejected) and not 255 (opacity ignored)
@@ -2786,7 +2893,8 @@ public void SvgCodec_Load_RectXPercentage_ThrowsInvalidDataException()
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2788 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2788, 100, 100));
}
///
@@ -2802,7 +2910,8 @@ public void SvgCodec_Load_RectWidthPercentage_ThrowsInvalidDataException()
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 100, 100));
+ using var stream2804 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream2804, 100, 100));
}
///
@@ -2834,7 +2943,8 @@ public void SvgCodec_Load_GradientStopOffsetNaN_DoesNotThrowAndRenders()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream2836 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2836, 100, 100);
// Assert: rendering completed without the raw ArgumentOutOfRangeException a non-finite
// offset reaching GradientStop's constructor would otherwise throw
@@ -2865,7 +2975,8 @@ public void SvgCodec_Load_GradientX1Infinity_FallsBackToDefaultAndRenders()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream2867 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2867, 100, 100);
// Assert: brightness still increases left to right, proving x1 fell back to 0 rather
// than Infinity (which would break or degenerate the ramp)
@@ -2902,7 +3013,8 @@ public void SvgCodec_Load_RgbaAlphaInfinity_TreatsColorAsUnrecognizedNoPaint()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream2904 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2904, 100, 100);
// Assert: the background white shows through - the Infinity-alpha rgba() color was
// rejected as unrecognized, not drawn as (saturated-opaque) black
@@ -2927,7 +3039,8 @@ public void SvgCodec_GetInfo_WidthHeightInfinity_FallsBackToDefaultSize()
const string svg = "";
// Act
- var info = SvgCodec.GetInfo(ToStream(svg));
+ using var stream2929 = ToStream(svg);
+ var info = SvgCodec.GetInfo(stream2929);
// Assert: falls back to the CSS/UA default replaced-element intrinsic size
Assert.Equal(300, info.Width);
@@ -2945,7 +3058,8 @@ public void SvgCodec_GetInfo_WidthHeightScientificNotation_ResolvesToBareValue()
const string svg = "";
// Act
- var info = SvgCodec.GetInfo(ToStream(svg));
+ using var stream2947 = ToStream(svg);
+ var info = SvgCodec.GetInfo(stream2947);
// Assert
Assert.Equal(100, info.Width);
@@ -2973,7 +3087,8 @@ public void SvgCodec_Load_GradientStopOffsetPercentage_RendersGradientCorrectly(
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream2975 = ToStream(svg);
+ var surface = SvgCodec.Load(stream2975, 100, 100);
// Assert: brightness increases left to right, as with the equivalent bare-number test
Assert.True(surface[10, 50].R < surface[90, 50].R);
@@ -3005,7 +3120,8 @@ public void SvgCodec_Load_UnsupportedConstructs_StillRendersRestOfDocument()
""";
// Act
- var surface = SvgCodec.Load(ToStream(svg), 100, 100);
+ using var stream3007 = ToStream(svg);
+ var surface = SvgCodec.Load(stream3007, 100, 100);
// Assert: the plain rect after the unsupported constructs still rendered
Assert.Equal(255, surface[20, 20].A);
@@ -3084,7 +3200,8 @@ public void SvgCodec_Load_NonPositiveWidth_PropagatesSurfaceArgumentOutOfRangeEx
const string svg = "";
// Act & Assert
- Assert.Throws(() => SvgCodec.Load(ToStream(svg), 0, 10));
+ using var stream3086 = ToStream(svg);
+ Assert.Throws(() => SvgCodec.Load(stream3086, 0, 10));
}
///
diff --git a/test/DemaConsulting.CanvasNet.Tests/Codecs/SvgFixtureTests.cs b/test/DemaConsulting.CanvasNet.Tests/Codecs/SvgFixtureTests.cs
index dc0b5010..38c70730 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Codecs/SvgFixtureTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Codecs/SvgFixtureTests.cs
@@ -18,15 +18,31 @@ public class SvgFixtureTests
{
///
/// The directory containing the SVG fixture corpus, copied to the test output directory
- /// by the test project's SvgFixtures\** content item.
+ /// by the test project's SvgFixtures\** content item. Path.Join is used
+ /// instead of purely to avoid CodeQL's
+ /// cs/path-combine rule, since Path.Join does not discard
+ /// when the second segment looks rooted.
///
- private static string AssetsPath => Path.Combine(AppContext.BaseDirectory, "SvgFixtures");
+ private static string AssetsPath => Path.Join(AppContext.BaseDirectory, "SvgFixtures");
///
/// The path to the real "Open Sans" TrueType font, copied to the test output directory by
- /// the test project's FontFixtures\** content item.
+ /// the test project's FontFixtures\** content item. Path.Join is used
+ /// instead of for the same CodeQL
+ /// reason as .
///
- private static string FontPath => Path.Combine(AppContext.BaseDirectory, "FontFixtures", "OpenSans-Regular.ttf");
+ private static string FontPath => Path.Join(AppContext.BaseDirectory, "FontFixtures", "OpenSans-Regular.ttf");
+
+ ///
+ /// Resolves a fixture file within . The file names always
+ /// originate from this class's own literals rather than external input, so
+ /// path-injection is not a concern here; Path.Join is used instead of
+ /// purely to avoid CodeQL's
+ /// cs/path-combine rule, since Path.Join does not discard
+ /// when looks rooted.
+ ///
+ private static string ResolveFixturePath(string fileName) =>
+ Path.Join(AssetsPath, fileName);
///
/// Proves that shapes.svg's four basic shapes (rect/circle/ellipse/polygon) each
@@ -37,7 +53,7 @@ public class SvgFixtureTests
public void SvgCodec_Load_ShapesFixture_RendersExpectedShapeColors()
{
// Arrange & Act: load the fixture at its native 1:1 viewBox-to-raster size
- var surface = SvgCodec.Load(Path.Combine(AssetsPath, "shapes.svg"), 100, 100);
+ var surface = SvgCodec.Load(ResolveFixturePath("shapes.svg"), 100, 100);
// Assert: each shape's expected solid color appears at a representative interior pixel
Assert.Equal(new Rgba32(255, 0, 0, 255), surface[25, 25]); // rect (red)
@@ -60,7 +76,7 @@ public void SvgCodec_Load_ShapesFixture_RendersExpectedShapeColors()
public void SvgCodec_Load_GroupsAndTransformsFixture_RendersAtTransformedPositions()
{
// Arrange & Act
- var surface = SvgCodec.Load(Path.Combine(AssetsPath, "groups-and-transforms.svg"), 100, 100);
+ var surface = SvgCodec.Load(ResolveFixturePath("groups-and-transforms.svg"), 100, 100);
// Assert: the first rect inherits its group's "orange" fill and renders at its
// translate(50,0)-mapped position, not its pre-translation local (0,0)-(20,20) position
@@ -83,7 +99,7 @@ public void SvgCodec_Load_GroupsAndTransformsFixture_RendersAtTransformedPositio
public void SvgCodec_Load_GradientFixture_RendersVaryingGradientColors()
{
// Arrange & Act
- var surface = SvgCodec.Load(Path.Combine(AssetsPath, "gradient.svg"), 100, 100);
+ var surface = SvgCodec.Load(ResolveFixturePath("gradient.svg"), 100, 100);
// Assert: the red channel increases monotonically from left to right, and is fully
// opaque throughout - proving a real gradient (not a flat fallback color) was rendered
@@ -106,7 +122,7 @@ public void SvgCodec_Load_GradientFixture_RendersVaryingGradientColors()
public void SvgCodec_Load_UseReferenceFixture_RendersAtOffsetPositionOnly()
{
// Arrange & Act
- var surface = SvgCodec.Load(Path.Combine(AssetsPath, "use-reference.svg"), 100, 100);
+ var surface = SvgCodec.Load(ResolveFixturePath("use-reference.svg"), 100, 100);
// Assert: the referenced 20x20 "teal" box renders at its use-offset (30,40) position
Assert.Equal(new Rgba32(0, 128, 128, 255), surface[40, 50]);
@@ -125,7 +141,7 @@ public void SvgCodec_Load_UseReferenceFixture_RendersAtOffsetPositionOnly()
public void SvgCodec_Load_ToleratesUnsupportedConstructFixture_StillRendersRemainingContent()
{
// Arrange & Act
- var surface = SvgCodec.Load(Path.Combine(AssetsPath, "tolerant-unsupported.svg"), 100, 100);
+ var surface = SvgCodec.Load(ResolveFixturePath("tolerant-unsupported.svg"), 100, 100);
// Assert: the rect still renders its "lime" fill despite the sibling element
Assert.Equal(new Rgba32(0, 255, 0, 255), surface[25, 25]);
@@ -148,7 +164,7 @@ public void SvgCodec_Load_TextFixtureWithRealFont_RendersVisibleGlyphInk()
var fonts = new Dictionary { ["Open Sans"] = font };
// Act: rasterize the fixture with the font dictionary supplied
- var surface = SvgCodec.Load(Path.Combine(AssetsPath, "text.svg"), 200, 80, fonts);
+ var surface = SvgCodec.Load(ResolveFixturePath("text.svg"), 200, 80, fonts);
// Assert: at least one pixel within the text's expected region (below and around the
// baseline at x=10, y=55, font-size 40) was actually painted (non-transparent)
@@ -191,7 +207,7 @@ public void SvgCodec_Load_TextFixtureWithRealFont_RendersVisibleGlyphInk()
public void SvgCodec_Load_SvgGradientFixture_RendersVaryingGradientAndPinkBackground()
{
// Arrange & Act: rasterize at the fixture's native 300x200 viewBox size
- var surface = SvgCodec.Load(Path.Combine(AssetsPath, "SvgGradient.svg"), 300, 200);
+ var surface = SvgCodec.Load(ResolveFixturePath("SvgGradient.svg"), 300, 200);
// Assert: the pink background rect is visible at a point clearly outside the "bar" use
// shape (y in [80,100]) and every gradient rect (rows at y in [30,70] and [110,190])
@@ -231,7 +247,7 @@ public void SvgCodec_Load_InkscapeFiltersFixture_ToleratesFiltersAndRendersFlowe
// Arrange & Act: rasterize at the fixture's native 600x1000 width/height - this must not
// throw despite every flower (other than the very first) referencing an unsupported
// element
- var surface = SvgCodec.Load(Path.Combine(AssetsPath, "InkscapeFilters.svg"), 600, 1000);
+ var surface = SvgCodec.Load(ResolveFixturePath("InkscapeFilters.svg"), 600, 1000);
// Assert: a petal of the second flower - translate(150,50), filter="url(#filter48)" -
// still renders its own "#ff8010" (255,128,16) fill, proving the unsupported
diff --git a/test/DemaConsulting.CanvasNet.Tests/Codecs/TiffCodecTests.cs b/test/DemaConsulting.CanvasNet.Tests/Codecs/TiffCodecTests.cs
index 71ae500b..ed9098fc 100644
--- a/test/DemaConsulting.CanvasNet.Tests/Codecs/TiffCodecTests.cs
+++ b/test/DemaConsulting.CanvasNet.Tests/Codecs/TiffCodecTests.cs
@@ -921,7 +921,8 @@ public void TiffCodec_Load_LittleEndianRgbSingleStrip_ReturnsExpectedPixels()
.WithStrips(raw)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms923 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms923);
Assert.Equal(width, surface.Width);
Assert.Equal(height, surface.Height);
@@ -947,7 +948,8 @@ public void TiffCodec_Load_BigEndianRgbSingleStrip_ReturnsExpectedPixels()
.WithStrips(raw)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms949 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms949);
Assert.Equal(new Rgba32(1, 2, 3, 255), surface[0, 0]);
Assert.Equal(new Rgba32(4, 5, 6, 255), surface[1, 0]);
@@ -976,7 +978,8 @@ public void TiffCodec_Load_RgbaWithExtraSamples_PreservesAlpha()
.WithStrips(raw)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms978 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms978);
Assert.Equal(new Rgba32(1, 2, 3, 128), surface[0, 0]);
Assert.Equal(new Rgba32(4, 5, 6, 64), surface[1, 0]);
@@ -1004,7 +1007,8 @@ public void TiffCodec_Load_Grayscale_ReturnsGrayPixelsWithFullAlpha()
.WithStrips(raw)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1006 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1006);
Assert.Equal(new Rgba32(100, 100, 100, 255), surface[0, 0]);
Assert.Equal(new Rgba32(200, 200, 200, 255), surface[1, 0]);
@@ -1027,7 +1031,8 @@ public void TiffCodec_Load_MultiStrip_ReturnsExpectedPixels()
.WithStrips(strip0, strip1, strip2, strip3)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1029 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1029);
Assert.Equal(new Rgba32(1, 1, 1, 255), surface[0, 0]);
Assert.Equal(new Rgba32(2, 2, 2, 255), surface[1, 0]);
@@ -1054,7 +1059,8 @@ public void TiffCodec_Load_MultiStripWithTwoRowsPerStrip_ReturnsExpectedPixels()
.WithStrips(strip0, strip1)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1056 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1056);
Assert.Equal(new Rgba32(10, 10, 10, 255), surface[0, 0]);
Assert.Equal(new Rgba32(20, 20, 20, 255), surface[0, 1]);
@@ -1076,7 +1082,8 @@ public void TiffCodec_Load_PackBitsCompressedStrip_DecodesUsingIndependentEncode
.WithStrips(compressed)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1078 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1078);
Assert.Equal(new Rgba32(1, 2, 3, 255), surface[0, 0]);
Assert.Equal(new Rgba32(4, 5, 6, 255), surface[1, 0]);
@@ -1099,7 +1106,8 @@ public void TiffCodec_Load_LzwCompressedStrip_DecodesUsingIndependentEncoder()
.WithStrips(compressed)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1101 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1101);
Assert.Equal(new Rgba32(1, 2, 3, 255), surface[0, 0]);
Assert.Equal(new Rgba32(4, 5, 6, 255), surface[1, 0]);
@@ -1127,7 +1135,8 @@ public void TiffCodec_Load_LzwCompressedRepeatingData_DecodesUsingIndependentEnc
.WithStrips(compressed)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1129 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1129);
for (var x = 0; x < width; x++)
{
@@ -1151,7 +1160,8 @@ public void TiffCodec_Load_DeflateCompressedStrip_DecodesUsingIndependentEncoder
.WithStrips(compressed)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1153 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1153);
Assert.Equal(new Rgba32(1, 2, 3, 255), surface[0, 0]);
Assert.Equal(new Rgba32(4, 5, 6, 255), surface[1, 0]);
@@ -1183,7 +1193,8 @@ public void TiffCodec_Load_PredictorWithLzw_DecodesUsingIndependentEncoder()
.WithStrips(compressed)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1185 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1185);
for (var x = 0; x < width; x++)
{
@@ -1209,7 +1220,8 @@ public void TiffCodec_Load_PredictorWithDeflate_DecodesUsingIndependentEncoder()
.WithStrips(compressed)
.Build();
- var surface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1211 = new MemoryStream(file);
+ var surface = TiffCodec.Load(ms1211);
for (var x = 0; x < width; x++)
{
@@ -1231,7 +1243,8 @@ public void TiffCodec_Load_BadByteOrderMark_ThrowsInvalidDataException()
file[0] = (byte)'X';
file[1] = (byte)'X';
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1233 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1233));
}
///
@@ -1241,7 +1254,8 @@ public void TiffCodec_Load_BadByteOrderMark_ThrowsInvalidDataException()
public void TiffCodec_Load_TruncatedHeader_ThrowsInvalidDataException()
{
var file = new byte[] { (byte)'I', (byte)'I', 42 };
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1243 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1243));
}
///
@@ -1252,7 +1266,8 @@ public void TiffCodec_Load_TruncatedStripData_ThrowsInvalidDataException()
{
var file = StandardRgbBuilder(false, 4, 4, 1, 4).WithStrips(new byte[] { 1, 2, 3 }).Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1254 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1254));
}
///
@@ -1273,7 +1288,8 @@ public void TiffCodec_Load_UnsupportedBitsPerSample_ThrowsInvalidDataException()
.WithStrips(new byte[] { 1, 2, 3, 4, 5, 6 })
.Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1275 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1275));
}
///
@@ -1294,7 +1310,8 @@ public void TiffCodec_Load_UnsupportedPhotometricPalette_ThrowsInvalidDataExcept
.WithStrips(new byte[] { 1 })
.Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1296 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1296));
}
///
@@ -1305,7 +1322,8 @@ public void TiffCodec_Load_UnsupportedCompression_ThrowsInvalidDataException()
{
var file = StandardRgbBuilder(false, 1, 1, 6, 1).WithStrips(new byte[] { 1, 2, 3 }).Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1307 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1307));
}
///
@@ -1322,7 +1340,8 @@ public void TiffCodec_Load_WidthExceedsMaxDimension_ThrowsInvalidDataException()
.WithStrips(new byte[width * 3])
.Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1324 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1324));
}
///
@@ -1338,7 +1357,8 @@ public void TiffCodec_Load_HeightExceedsMaxDimension_ThrowsInvalidDataException(
.WithStrips(new byte[height * 3])
.Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1340 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1340));
}
///
@@ -1359,7 +1379,8 @@ public void TiffCodec_Load_PlanarConfigurationPlanar_ThrowsInvalidDataException(
.WithStrips(new byte[] { 1, 2, 3 })
.Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1361 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1361));
}
///
@@ -1380,7 +1401,8 @@ public void TiffCodec_Load_TiledTiff_ThrowsInvalidDataException()
.Add(TagTileLength, TypeLong, 16)
.Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1382 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1382));
}
///
@@ -1406,7 +1428,8 @@ public void TiffCodec_GetInfo_Seekable_TiledTiff_ThrowsInvalidDataException()
.Add(TagTileLength, TypeLong, 16)
.Build();
- Assert.Throws(() => TiffCodec.GetInfo(new MemoryStream(file)));
+ using var ms1408 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.GetInfo(ms1408));
}
///
@@ -1504,7 +1527,8 @@ public void TiffCodec_Load_MissingMandatoryTag_ThrowsInvalidDataException(ushort
var file = builder.Build();
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1506 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1506));
}
///
@@ -1518,7 +1542,8 @@ public void TiffCodec_GetInfo_Seekable_Rgb_ReturnsExpectedInfoWithoutAlpha()
.WithStrips(new byte[3 * 3 * 2])
.Build();
- var info = TiffCodec.GetInfo(new MemoryStream(file));
+ using var ms1520 = new MemoryStream(file);
+ var info = TiffCodec.GetInfo(ms1520);
Assert.Equal(new ImageInfo(3, 2, 3, false), info);
}
@@ -1535,7 +1560,8 @@ public void TiffCodec_GetInfo_Seekable_Rgba_ReturnsExpectedInfoWithAlpha()
.WithStrips(new byte[3 * 4 * 2])
.Build();
- var info = TiffCodec.GetInfo(new MemoryStream(file));
+ using var ms1537 = new MemoryStream(file);
+ var info = TiffCodec.GetInfo(ms1537);
Assert.Equal(new ImageInfo(3, 2, 4, true), info);
}
@@ -1581,7 +1607,8 @@ public void TiffCodec_GetInfo_NonSeekableStream_SucceedsAndMatchesLoadResult()
.WithStrips(new byte[width * height * 3])
.Build();
using var nonSeekableStream = new FunctionallySeekableButCanSeekFalseStream(new MemoryStream(file));
- var loadedSurface = TiffCodec.Load(new MemoryStream(file));
+ using var ms1583 = new MemoryStream(file);
+ var loadedSurface = TiffCodec.Load(ms1583);
// Act
var info = TiffCodec.GetInfo(nonSeekableStream);
@@ -1676,7 +1703,8 @@ public void TiffCodec_GetInfo_BadByteOrderMark_ThrowsInvalidDataException()
bytes[0] = (byte)'X';
bytes[1] = (byte)'X';
- Assert.Throws(() => TiffCodec.GetInfo(new MemoryStream(bytes)));
+ using var ms1678 = new MemoryStream(bytes);
+ Assert.Throws(() => TiffCodec.GetInfo(ms1678));
}
///
@@ -1692,10 +1720,12 @@ public void TiffCodec_GetInfo_OversizedDimensions_ReturnsRawValue_ButLoadThrows(
.WithStrips(new byte[width * 3])
.Build();
- var info = TiffCodec.GetInfo(new MemoryStream(file));
+ using var ms1694 = new MemoryStream(file);
+ var info = TiffCodec.GetInfo(ms1694);
Assert.Equal(width, info.Width);
- Assert.Throws(() => TiffCodec.Load(new MemoryStream(file)));
+ using var ms1697 = new MemoryStream(file);
+ Assert.Throws(() => TiffCodec.Load(ms1697));
}
///