Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ words:
- Cmaps
- subtables
- bilinearly
- stackalloc
- prefilter
- prefiltered
- resampler
- Catmull
- Lanczos
- optimizespeed
- optimizequality
- sinc
- Strikethru
- Strikethrough
- linepattern
Expand Down
8 changes: 8 additions & 0 deletions .reviewmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,13 @@ reviews:
- "docs/verification/canvas-net/canvas/surface.md" # unit verification
- "src/DemaConsulting.CanvasNet/Canvas/Surface.cs" # unit source
- "src/DemaConsulting.CanvasNet/Canvas/SurfaceResampler.cs" # internal image resampling helper
- "src/DemaConsulting.CanvasNet/Canvas/SurfaceResampler.Engine.cs" # separable resampling engine
- "src/DemaConsulting.CanvasNet/Canvas/ResamplingMode.cs" # public resampling filter enum
- "src/DemaConsulting.CanvasNet/Canvas/NamespaceDoc.cs" # Canvas namespace documentation
- "test/DemaConsulting.CanvasNet.Tests/Canvas/SurfaceTests.cs" # unit tests
- "test/DemaConsulting.CanvasNet.Tests/Canvas/SurfaceResamplerTests.cs" # resampler unit tests
- "test/DemaConsulting.CanvasNet.Tests/Canvas/SurfaceResizeTests.cs" # resize unit tests
- "test/DemaConsulting.CanvasNet.Tests/Canvas/SurfaceResizeCodecRoundTripTests.cs" # round-trip tests

# CanvasNet-Canvas-Rgba32 Review (one per unit)
- id: CanvasNet-Canvas-Rgba32
Expand Down Expand Up @@ -708,6 +712,8 @@ reviews:
- "src/DemaConsulting.CanvasNet.Svg/SvgCodec.Filters.Blend.cs" # phase 8 filter partial
- "src/DemaConsulting.CanvasNet.Svg/NamespaceDoc.cs" # Svg namespace documentation
- "test/DemaConsulting.CanvasNet.Svg.Tests/SvgCodecTests.cs" # unit tests
- "test/DemaConsulting.CanvasNet.Svg.Tests/SvgImageResamplingTests.cs" # image resampling unit tests
- "test/DemaConsulting.CanvasNet.Svg.Tests/SvgPatternTileSmoothingTests.cs" # pattern tile sampling unit tests
- "test/DemaConsulting.CanvasNet.Svg.Tests/SvgFixtureTests.cs" # fixture conformance tests

# CanvasNetPdf-Architecture Review (one per system)
Expand Down Expand Up @@ -801,6 +807,8 @@ reviews:
- "test/DemaConsulting.CanvasNet.Pdf.Tests/PdfDocumentJpxTests.cs" # JPXDecode/SMask unit tests
- "test/DemaConsulting.CanvasNet.Pdf.Tests/PdfDocumentThinRuleInkTests.cs" # thin-rule ink unit tests
- "test/DemaConsulting.CanvasNet.Pdf.Tests/PdfDocumentImageDownscaleTests.cs" # image downscale unit tests
- "test/DemaConsulting.CanvasNet.Pdf.Tests/PdfDocumentImageColorConversionTests.cs" # image color tests
- "test/DemaConsulting.CanvasNet.Pdf.Tests/PdfImageResamplingTests.cs" # image resampling grid unit tests
- "test/DemaConsulting.CanvasNet.Pdf.Tests/PdfDocumentHairlineStrokeTests.cs" # hairline stroke unit tests
- "test/DemaConsulting.CanvasNet.Tests/Jpeg2000Fixtures/gray.jp2" # gray JPX fixture
- "test/DemaConsulting.CanvasNet.Tests/Jpeg2000Fixtures/source_gray.png" # source pixels of gray.jp2
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@

CanvasNet is a .NET library providing a mutable, span-based 32-bit RGBA pixel buffer along with
codecs for loading and saving images in common file formats. It's designed for fast, allocation-conscious
image operations using `Span<T>`, and supports independent-copy cropping for load/crop/save workflows.
image operations using `Span<T>`, and supports independent-copy cropping and high-quality resizing for
load/crop/resize/save workflows.

## Features

- 🖼️ **Pixel Buffer** - Mutable 32-bit RGBA surface with span access
- ✂️ **Cropping** - Independent-copy crop for load/crop/save workflows
- 📐 **Resizing** - `Resize`/`ResizeToFit` with Nearest, Bilinear, Bicubic, Lanczos3 and Area filters
- 🌈 **Compositing** - Alpha premultiply and Porter-Duff "over" compositing
- 📀 **BMP Codec** - Load/save 24-bit/32-bit uncompressed BMP files
- 🎨 **PNG Codec** - Load non-interlaced PNGs; save 8-bit RGB/RGBA
Expand Down Expand Up @@ -87,6 +89,12 @@ using var cropped = surface.Crop(0, 0, 2, 2);
// Save as PNG and load it back
PngCodec.Save(surface, "surface.png");
using var reloaded = PngCodec.Load("surface.png");

// Load, crop, resize to fit a 64x64 box, and save
using var photo = PngCodec.Load("photo.png");
using var region = photo.Crop(10, 10, 200, 100);
using var thumbnail = region.ResizeToFit(64, 64, ResamplingMode.Lanczos3);
PngCodec.Save(thumbnail, "thumbnail.png");
```

Open a PDF and render a page (BMP/TIFF/JPEG/GIF/JPEG 2000/SVG codecs, header-only probing, and
Expand Down
37 changes: 21 additions & 16 deletions docs/design/canvas-net-pdf/pdf-document.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ glyph under a clip mode fails closed); and no additional stream filters were add
**Phase 3 limitations** (narrowed by Phase 7/13 and the `/Pattern` color-space phase, see below):
there are no transparency groups, `/Mask` (stencil/color-key) image masks and `/Matte` are not
consulted, and the Phase 3 operator subset is otherwise limited as documented below. These remain
out of scope and are silently skipped (any other undefined keyword) or explicitly rejected
out of scope (along with `/ImageMask` stencil images, which are not rendered) and are silently
skipped (any other undefined keyword) or explicitly rejected
(unsupported color spaces/filters/fonts/encodings/render modes), per the operator/exception
taxonomy documented below. Since then the following have been added: shading/tiling pattern fills
(`/ShadingType 2`/`3`, `/PatternType 1`/`2` — see _`/Pattern` Color Space (Shading and Tiling
Expand Down Expand Up @@ -672,6 +673,9 @@ its own distinguishable `Feature` token.
number, resolves `lookup` (a PDF string's raw bytes, or a stream decoded via
`GetStreamDecodedBytes`), and validates the resolved palette has at least
`(Hival + 1) * ComponentCount(baseSpace)` bytes (else `InvalidDataException` in each case).
Image samples are converted without a per-pixel array: `SamplesToColor` and `IndexedToColor`
place the (at most four) components in a `stackalloc` span and call
`ColorFromComponentSpan`, which applies the same conversion as the `IReadOnlyList` overload.
`ComponentCount` reports `1` for `Indexed` (the palette index itself); `ColorFromComponents`
dispatches an `Indexed` color's single raw (not `[0, 1]`-normalized) index component to
`IndexedToColor`, which rounds/clamps the index into `[0, Hival]` (an out-of-range index
Expand Down Expand Up @@ -768,24 +772,24 @@ its own distinguishable `Feature` token.
`pdf-jpx-smaskindata-indexed`) and then applies it; other values throw
`InvalidDataException`. An explicit `/SMask` image (any filter, including `JPXDecode`; decoded
through the same `DecodeImageSamples` path without nested masks, and required to be a
single-component non-`/Indexed` image, else `InvalidDataException`) is nearest-neighbor resampled
single-component non-`/Indexed` image, else `InvalidDataException`) is resized smoothly (bilinear)
to the base image and its luminance multiplies the base alpha in place on the base surface
(`ApplySoftMask`), taking
precedence over `/SMaskInData`; `/Mask` and `/Matte` are not consulted. Inline images (`BI`) are
not supported at all, which satisfies the PDF rule that `JPXDecode` is not permitted inline.
`CompositeImageOntoSurface` inverts the CTM (a non-invertible/degenerate CTM silently paints
nothing), computes the device-space bounding box of the transformed unit square, and for every
destination pixel in that box nearest-neighbor-samples the source image (`row = floor((1 - v) *
image.Height)`, since image sample row `0` is the _top_ of the unit square per the PDF
specification's image-space convention, the opposite of user-space's y-up convention). When the
image is enlarged or drawn 1:1 this nearest-neighbor lookup is unchanged. When the pixel-to-device
mapping shrinks the image on either axis (local scales are the column norms of the 2x2 part of
the matrix), the internal `Canvas.SurfaceResampler` first area-averages the image once per draw
(an integer box prefilter, factor = nearest integer to the reduction ratio, premultiplied alpha,
output no larger than the source, so memory is bounded by the decoded image) and the result is
sampled bilinearly (centre-aligned, edge-clamped); mild shrinks below 1.5:1 use bilinear on the
source directly. Thin features such as one-pixel rules are therefore attenuated rather than
dropped. `/Interpolate` is not consulted. Each sampled source pixel is alpha-blended "over" the
nothing), computes the device-space bounding box of the transformed unit square, and when the
placement is axis-aligned (including flips) the internal `Canvas.SurfaceResampler.TryResampleToDevice`
resamples the image once per draw to exactly the device window it covers (the shared separable,
premultiplied-alpha engine: `Area` when shrinking, `Bicubic` when enlarging, kernel support scaled
by the reduction ratio, window clipped to the page so memory is bounded) and the window is composited
1:1, with the clip coverage applied (image sample row `0` is the _top_ of the unit square per the PDF
specification's image-space convention, the opposite of user-space's y-up convention). A rotated or
skewed placement falls back to inverse-mapped bilinear sampling of an area-prefiltered source
(`Prefilter` + `SampleBilinear`). Thin features such as one-pixel rules are therefore attenuated
rather than dropped, and enlarged images are interpolated rather than shown as blocks.
`/Interpolate` is not consulted.
Each sampled source pixel is alpha-blended "over" the
existing destination pixel via the shared internal `Canvas.Rgba32.CompositeOver(Rgba32, Rgba32)`
helper (standard Porter-Duff "over" alpha compositing — straight/unassociated alpha in and out,
`outA = fgA + bgA * (1 - fgA)`, each color channel `outC = (fgC * fgA + bgC * bgA * (1 - fgA)) /
Expand Down Expand Up @@ -1446,8 +1450,9 @@ its own distinguishable `Feature` token.
caller-supplied tint before compositing, preserving each pixel's own painted alpha exactly (so a
partially transparent cell stays partially transparent, just recolored). The rendered cell
becomes a `Drawing.TilePaint` (new `Drawing` subsystem unit — see _Introduction_ and
`Drawing/TilePaint.cs`/`Drawing/TilePaintEvaluator.cs`), sampled nearest-neighbor per destination
pixel by `Drawing.PathFiller`'s new `TilePaint` fill overload, mirroring the existing `Gradient`
`Drawing/TilePaint.cs`/`Drawing/TilePaintEvaluator.cs`), sampled per destination
pixel (exact lookup at 1:1, smoothed when magnified or minified) by `Drawing.PathFiller`'s new `TilePaint` fill
overload, mirroring the existing `Gradient`
overload's structure exactly.
- **Mesh shadings (`PdfDocument.Patterns.Shading.Mesh.cs`, `/ShadingType` 4-7)** — `ResolveMeshShading`
requires the shading to be a stream and validates `/BitsPerCoordinate` (1/2/4/8/12/16/24/32),
Expand Down
18 changes: 13 additions & 5 deletions docs/design/canvas-net-pptx/pptx-document.md
Original file line number Diff line number Diff line change
Expand Up @@ -848,11 +848,19 @@ resolves a `<p:pic>` shape's `<p:blipFill>/<a:blip>` into a decoded core `Surfac
- **Painting**: `PaintPicture(Surface destination, Surface image, PptxSrcRect? srcRect, Matrix3x2
shapeTransform, float widthEmu, float heightEmu, Path? clipPath = null)` composes the crop
rectangle's own crop-to-unit-square mapping with `shapeTransform` into a single
image-to-surface matrix, inverts it once, and for every destination pixel whose inverse-mapped
coordinate falls within `[0,1]x[0,1]` samples the source image with nearest-neighbor filtering
(no bilinear/anti-aliased resampling this phase - a documented simplification, consistent with
this package's existing "no anti-aliasing yet" posture established by `PaintTextLayout`'s own
glyph-ink fill). Each sampled source pixel is alpha-blended "over" the existing destination
image-to-surface matrix, and for an axis-aligned placement resamples the cropped region once with
`SurfaceResampler.TryResampleToDevice` (shared separable engine: `Area` when shrinking, `Bicubic` when
enlarging) and composites the resulting device window 1:1. The `srcRect` crop is isolated first: the smallest
whole-pixel window of the source containing the crop is cut out (`Surface.Crop`), so kernel taps next to a
crop edge fold into the window's own edge pixels and pixels outside the window never bleed into it. The
window is a whole-pixel superset of the (possibly fractional) crop, so a partially covered border pixel
still contributes to the crop edge; the
fractional crop offset is part of the mapping. A crop that overhangs the image (negative `srcRect`
edges) takes the inverse-mapped path below instead, whose edge-clamped sampling replicates the source
edge pixels across the overhang. A rotated picture inverts the matrix once and
bilinear-samples an area-prefiltered copy of the same cropped window per destination pixel whose
inverse-mapped coordinate falls within `[0,1]x[0,1]`. Each sampled source pixel is
alpha-blended "over" the existing destination
pixel via the shared internal `Canvas.Rgba32.CompositeOver(Rgba32, Rgba32)` helper (standard
Porter-Duff "over" alpha compositing - straight/unassociated alpha in and out, `outA = fgA +
bgA * (1 - fgA)`, each color channel `outC = (fgC * fgA + bgC * bgA * (1 - fgA)) / outA` when
Expand Down
27 changes: 18 additions & 9 deletions docs/design/canvas-net-svg/svg-codec.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ inherited, only stops/content are" simplification); a rendered tile-buffer cache
fill/stroke re-renders its own tile from scratch, since a tile's own pixel size depends on the
referencing shape's own bounding box, not only the `pattern` element itself); a dedicated
pattern-reference cycle-depth counter (a pattern-content cycle is instead caught for free by the
pre-existing `MaxElementDepth` guard, mirroring `mask`'s identical reliance); and bilinear/other
non-nearest-neighbor tile resampling (matching this codec's existing "no resampling filter
anywhere" convention). Within the supported `image` feature itself, the following are explicitly
pre-existing `MaxElementDepth` guard, mirroring `mask`'s identical reliance); and per-element
control of tile resampling beyond the automatic magnified/minified smoothing described below. Within the supported
`image` feature itself, the following are explicitly
out of scope (see _Image_ below for the full rationale behind each): an `href`/`xlink:href` that
is not a base64-encoded `data:` URI - an external file path or URL is tolerantly rendered as
nothing, since resolving it would require introducing a base-path/resolver mechanism that does
Expand All @@ -166,8 +166,9 @@ nested SVG document, whether base64-encoded or not) - tolerantly rendered as not
same recursive-parsing-complexity reasoning that already excludes `feImage`'s own
referenced-element case; auto-sizing from the decoded raster's own intrinsic dimensions when
`width`/`height` are absent (both default to `0`, rendering nothing, rather than inferring a size
from the decoded content); and bilinear/other non-nearest-neighbor image resampling (matching
`pattern`'s identical "no resampling filter anywhere" convention).
from the decoded content); and `image-rendering` values other than the recognized keywords
(`auto`, `optimizeQuality`, `smooth` and `high-quality` select smooth sampling; `pixelated`,
`crisp-edges` and `optimizeSpeed` select nearest-neighbor sampling), which are ignored.

A percentage value on a shape/text geometry attribute (`x`, `y`, `width`, `height`, `rx`, `ry`,
`cx`, `cy`, `r`, `x1`/`y1`/`x2`/`y2`, `font-size`, `stroke-width`, `stroke-dasharray`,
Expand Down Expand Up @@ -1103,8 +1104,10 @@ genuinely independent attributes elsewhere in this codec. Third, `SampleTileInto
the rendered tile buffer into a region-sized destination, one destination pixel at a time: each
pixel's own center is inverse-transformed back into grid space, wrapped (modulo) into the tile's
own `[0, width) x [0, height)` extent via `WrapCoordinate`, rescaled into the tile buffer's own
pixel coordinates, and sampled nearest-neighbor (matching this codec's existing "no bilinear
resampling anywhere" convention) - new, isolated, well-commented arithmetic with no existing
pixel coordinates, and sampled according to the tile scale: exact nearest lookup when the tile is drawn at about one
device pixel per tile pixel (within 5 percent), wrapped bilinear when magnified, and an `Area`-resized copy of the tile
(disposed after the fill) when minified (the sampling step is internal so that unit tests can exercise
each scale regime directly) - new, isolated, well-commented arithmetic with no existing
precedent to reuse, since this is a pure C# rasterizer with no native tiled/bitmap-shader
primitive of any kind. That sample's own alpha is then multiplied by a freshly rasterized
antialiased fill/stroke coverage buffer (`PathFiller.Fill` into a fresh `Surface`, the same
Expand Down Expand Up @@ -1173,8 +1176,14 @@ to compute a fit transform from the decoded image's own intrinsic pixel size int
rect, composed as `fitTransform * Matrix3x2.CreateTranslation(x, y) * elementTransform` (matching
this codec's established left-to-right `A * B` "apply `A` first" composition convention). Each
destination pixel within the placement rect's own pixel bounds is inverse-transformed back into
the decoded image's own pixel space and sampled nearest-neighbor (matching this codec's existing
"no bilinear resampling anywhere" convention) - a pixel whose inverse-transformed coordinate falls
the decoded image's own pixel space and resampled through the shared engine (`SurfaceResampler.TryResampleToDevice`:
`Area` when shrinking,
`Bicubic` when enlarging) for axis-aligned placements, with an inverse-mapped bilinear fallback on a
prefiltered source for rotated or skewed ones; the `image-rendering` property (attribute, style
declaration or stylesheet, inherited) set to `pixelated`, `crisp-edges` or `optimizeSpeed` selects
`Nearest` instead, while `auto`, `optimizeQuality`, `smooth` and `high-quality` select smooth
resampling and any other (unrecognized) value is ignored, so the element inherits its parent's
setting (`feImage` always smooths) - a pixel whose inverse-transformed coordinate falls
outside the decoded image's own `[0, width) x [0, height)` extent (possible under a `slice` fit,
or simply near the fit rect's own edges) is left untouched rather than wrapped, unlike `pattern`'s
own deliberate wrap-around tiling. The result is composited onto the real canvas via
Expand Down
Loading
Loading