You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add Surface.Resize, smooth image resampling for documents, and profile-driven optimizations - #47
Adds public image resizing and uses the same high-quality resampler for images embedded in documents, followed by profile-driven performance work.
Public API:ResamplingMode (Auto, Nearest, Bilinear, Bicubic, Lanczos3, Area), Surface.Resize and Surface.ResizeToFit. This makes load/crop/resize/save straightforward. Alpha is handled premultiplied.
Documents: PDF images, PPTX pictures, SVG <image>, feImage, SVG pattern tiles and PDF tile paint now use the shared engine. This is area averaging when shrinking and bicubic when enlarging. SVG image-rendering: pixelated|crisp-edges|optimizeSpeed selects Nearest. PDF /SMask resizing is now bilinear.
Performance: rasterizer empty-row and zero-column skipping, TensorPrimitives clip multiply, a per-thread compositing scratch cache, pooled resampler buffers, a glyph outline cache and PathBuilder scratch reuse. Warm 96 dpi renders: BB6 page 1 123 to 66 ms, BB6 page 0 56-77 to 36 ms, issue 9 repro 24.8 to 20.7 ms. Allocation on the issue 9 repro fell from 62.8 to 15.4 MB. A golden-pixel diff was byte-identical. Candidates the profile did not justify were rejected: the opaque compositing fast path, vectorized coverage resolve, frozen collections and parallel resampler strips.
Docs: user guide and README example, design, verification, requirements, review sets.
Known limits: PDF /ImageMask and /Interpolate are not implemented. About 40 existing exact-value test assertions were loosened to tolerances because smoothing changes edge pixels (justified in the tests and verification docs).
Type of Change
Bug fix (non-breaking change which fixes an issue)
New feature (non-breaking change which adds functionality)
Breaking change (fix or feature that would cause existing functionality to not work as expected)
Documentation update
Code quality improvement
Related Issues
Closes #
Pre-Submission Checklist
Before submitting this pull request, ensure you have completed the following:
Build and Test
Code builds successfully and all tests pass: pwsh ./build.ps1 (passed, 13821 tests, 0 failed, net8/net9/net10)
Code produces zero warnings
Code Quality
New code has appropriate XML documentation comments
Static analyzer warnings have been addressed
Quality Checks
Please run the following checks before submitting:
All linters pass: pwsh ./lint.ps1 (exit 0)
Testing
Added unit tests for new functionality
Updated existing tests if behavior changed
All tests follow the AAA (Arrange, Act, Assert) pattern
Test coverage is maintained or improved
Documentation
Updated README.md (if applicable)
Updated docs/ documentation (if applicable)
Added code examples for new features (if applicable)
Updated requirements.yaml (if applicable)
Additional Notes
Reviewed locally before pushing: code review, formal review of all affected ReviewMark sets including system level, traceability check (no orphaned requirements), lint-fix pass. Existing tests contain no timing assertions, and none were added.
Add ResamplingMode and a separable, premultiplied-alpha resampling engine; route
PDF, PPTX, SVG images and tile paints through it, with tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This PR adds public image resizing to CanvasNet and shares its resampling engine with document renderers. It also targets rendering and allocation costs identified by profiling.
Changes:
Add Surface.Resize, ResizeToFit, and selectable filters.
Smooth raster images and tiles in PDF, PPTX, and SVG rendering.
Add rendering optimizations, tests, and supporting documentation and requirements.
For an EvenOdd row with multiple vertical breakpoints and no dense clip, this sets the window to the full clip width, then ApplyClip returns before trimming because _clip is null. Even a narrow shape or an all-zero row is passed to paint evaluation and compositing across the entire width, undoing the new skip/window optimization on this path. Trim the resolved row even when there is no clip, and set the length to zero when the row has no coverage.
Prevent resize alpha-edge tests from passing without visible pixels
This loop checks RGB only when p.A > 0, so it passes without any color assertions if resizing makes every pixel transparent. The per-filter Resize_AlphaEdge_HasNoColorFringe test in SurfaceResizeTests.cs has the same gap. Assert nonzero or expected alpha at a known visible pixel in both tests, then check the colors of visible pixels; that makes a dropped alpha edge fail instead of passing vacuously.
The finite/nonzero check still lets an axis-aligned placement produce a source footprint beyond long range. For example, an 8192-pixel-wide image placed at x=0.5 with scale 1e-19 selects device pixel 0, but this call passes a source extent of 1e19 to BuildAxis, which casts its tap bounds to long; the resulting weights can select an edge pixel instead of averaging the image. At less extreme scales, the bounded-tail bucket calculations can also overflow long for Bilinear/Bicubic/Lanczos3. Bound or reject these footprints before the integer tap math, and make tail bucket arithmetic overflow-safe so tiny placements cannot silently produce wrong pixels.
Limit cached glyph geometry size, not just glyph entry count
This admission check caps the number of glyphs but not the size of their retained outlines. GlyfLocaReader allows up to 200,000 points/commands per decoded glyph, so an untrusted font with thousands of distinct large glyphs can leave a very large amount of geometry resident as long as the TrueTypeFont is retained, even though the cache remains below 4096 entries. Limit the total cached geometry cost (or skip/evict oversized outlines), not just the entry count.
Update SysML2 unit models and artifact paths for resampling changes
The SysML2 unit model was not updated alongside the new APIs and renderer behavior. docs/sysml2/model/canvas-net/canvas/surface.sysml still describes only cropping/compositing and omits ResamplingMode.cs, SurfaceResampler.Engine.cs, and the resize test files from its artifact comments; drawing/tile-paint.sysml still says tiles are sampled only by nearest neighbor, and the PDF/SVG unit test artifact lists omit their new resampling test files. These files are the repository's architecture and artifact lookup records, so queries now return stale behavior and incomplete test locations. Update the affected unit descriptions and artifact paths to match this change.
…eature guarantee
Map fallback sample coordinates by block factor (x / factor) instead of
uniform normalization so a partial last block no longer shifts samples.
Applies to PPTX, SVG and PDF. Qualify the thin-feature guarantee to
filtering modes other than Nearest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nearest: false gives raster feImage its own smooth-resampling path, but the new SVG resampling tests exercise only <image> elements. The existing feImage data-URI test uses a solid blue source, which renders the same with nearest and smooth sampling. Add a high-contrast feImage enlargement or reduction that checks an interpolated boundary or area-averaged pixel, so a regression to nearest sampling is detected.
ResolveSrcRect accepts negative crop edges (including the l="-4697" value in PptxRenderTests.cs). For a negative left or top edge, clamping windowX0/windowY0 to zero here and mapping only that source window into the shape makes TryResampleToDevice skip the overhanging strip. Previously the inverse-mapped lookup painted that strip using the source edge pixel. Axis-aligned pictures can therefore show a transparent band where they used to show image content. Keep the full shape footprint and replicate the source edge into overhanging crop regions, or route those crops through an edge-clamped fallback.
Incorrect sampling at partial trailing reduction blocks
When BoxReduce makes a partial last block (for example, width 5 and factor 2), SampleBilinearReduced divides every source coordinate by the full factor. At the last source pixel's center (x = 4.5), that produces reduced coordinate 2.25; SampleBilinear then blends the preceding block into the last block instead of sampling the last block at its center. This shifts edge colors in rotated or skewed PDF, PPTX, and SVG images that use the prefiltered fallback. Account for the actual width and center of a partial trailing block when mapping source coordinates to reduced pixels, and cover a non-divisible reduction in a fallback-path test.
A valid PPTX crop can include a source column that lies entirely outside its boundary. For a 45-pixel-wide image with a 40% right crop, cropWidth is computed as 0.6000000238f, so the exclusive pixel endpoint becomes 27.00000107; subtracting 1e-6 still leaves it above 27, and Ceiling(...)-1 includes column 27 instead of ending at 26. When that column differs in color, area or bicubic resampling can pull it into the cropped picture's right edge. Compute the window bounds with precision or a tolerance appropriate to the image size, and test a non-binary fractional crop that lands at an integer pixel boundary.
Compute crop fractions in double and snap near-integer pixel boundaries before Floor/Ceiling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This out-of-scope statement contradicts both ParseImageRendering in SvgCodec.Elements.cs and the later image design section: optimizeQuality, smooth, and high-quality are recognized and select smooth sampling, including when overriding an inherited pixelated value. List those supported keywords here, reserving the out-of-scope statement for genuinely unsupported values.
The shear check multiplies the off-diagonal terms by the wrong source dimensions. In Matrix3x2, M12 shifts device Y across the source width, and M21 shifts device X across the source height. For example, a 1000×10 image with M12 = 0.001 shifts one device pixel vertically, but this check sees only 0.01 and accepts the axis-aligned path; that path ignores the shear and can paint the wrong pixels. Compare abs(M12) * source.Width and abs(M21) * source.Height against the tolerance, and test a non-square, slightly sheared image so it takes the inverse-mapped path.
With a fractional srcRect, source includes whole border pixels beyond the crop. pixelToSurface maps that entire window to device coordinates, and CompositeWindow paints all of it without restricting pixels to the picture's unit-square footprint. For example, a 4-pixel image cropped 12.5% on the left and placed at x=10 with width 40 generates a window starting near x=4.3, painting pixels left of x=10. Keep the extra source pixels for filter taps, but clip the axis-aligned output to the actual picture footprint before compositing; the inverse-mapped fallback already checks u and v against [0,1).
Matrix3x2 applies M12 to the source x coordinate and M21 to the source y coordinate. This check uses the opposite dimensions, so, for example, a 1000×1 image with M12 = 0.001 passes as axis-aligned even though its far edge moves one device pixel vertically. TryResampleToDevice then drops that rotation/skew and paints the wrong pixels. Compare abs(M12) * source.Width and abs(M21) * source.Height with the tolerance, and correct the matching tolerance description in the design document.
The new CompositeSpanWorkspace_DisposeEvictDispose_ReturnsArraysAtMostOnce test (SurfaceTests.cs:1471) checks that evicting a cached workspace and disposing a stale reference cannot return its arrays twice. It is missing from both this requirement's tests list and the matching scenario in docs/verification/canvas-net/canvas/surface.md:394-397. Add it to both lists and describe the eviction/disposal check in the verification scenario so this cache-safety regression is traceable.
Add outline budget test to GlyphOutlineMemoized requirement
The new GlyphOutlineMemoized requirement lists the other cache tests but omits TrueTypeFont_GetGlyphOutline_OutlineExceedingCostBudget_IsNotCached. That test is already listed in docs/verification/canvas-net/fonts/true-type-font.md:261-264 and checks that an outline exceeding the retained-geometry budget is decoded but not cached. Add it to this requirement's tests list so the budget behavior has a consistent ReqStream trace.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Description
Adds public image resizing and uses the same high-quality resampler for images embedded in documents, followed by profile-driven performance work.
ResamplingMode(Auto, Nearest, Bilinear, Bicubic, Lanczos3, Area),Surface.ResizeandSurface.ResizeToFit. This makes load/crop/resize/save straightforward. Alpha is handled premultiplied.<image>,feImage, SVG pattern tiles and PDF tile paint now use the shared engine. This is area averaging when shrinking and bicubic when enlarging. SVGimage-rendering: pixelated|crisp-edges|optimizeSpeedselects Nearest. PDF/SMaskresizing is now bilinear.TensorPrimitivesclip multiply, a per-thread compositing scratch cache, pooled resampler buffers, a glyph outline cache andPathBuilderscratch reuse. Warm 96 dpi renders: BB6 page 1 123 to 66 ms, BB6 page 0 56-77 to 36 ms, issue 9 repro 24.8 to 20.7 ms. Allocation on the issue 9 repro fell from 62.8 to 15.4 MB. A golden-pixel diff was byte-identical. Candidates the profile did not justify were rejected: the opaque compositing fast path, vectorized coverage resolve, frozen collections and parallel resampler strips.Known limits: PDF
/ImageMaskand/Interpolateare not implemented. About 40 existing exact-value test assertions were loosened to tolerances because smoothing changes edge pixels (justified in the tests and verification docs).Type of Change
Related Issues
Closes #
Pre-Submission Checklist
Before submitting this pull request, ensure you have completed the following:
Build and Test
pwsh ./build.ps1(passed, 13821 tests, 0 failed, net8/net9/net10)Code Quality
Quality Checks
Please run the following checks before submitting:
pwsh ./lint.ps1(exit 0)Testing
Documentation
Additional Notes
Reviewed locally before pushing: code review, formal review of all affected ReviewMark sets including system level, traceability check (no orphaned requirements), lint-fix pass. Existing tests contain no timing assertions, and none were added.