Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
root = true

[*.cs]
# Public API baseline (Microsoft.CodeAnalysis.PublicApiAnalyzers, see Directory.Build.props): an API that is not in
# PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt, or one listed there that no longer exists, fails the build. This makes
# API changes visible in review; it does not forbid them: update the files (and the release notes for a break).
dotnet_diagnostic.RS0016.severity = error
dotnet_diagnostic.RS0017.severity = error

# The same analyzer's overload-shape rules (RS0026: several overloads with optional parameters; RS0027: the optional
# parameters on an overload with fewer parameters) are API style, not the baseline. The codecs deliberately offer
# byte[] and ReadOnlySpan<byte> overloads with the same optional options; they are not changed for this rule.
dotnet_diagnostic.RS0026.severity = none
dotnet_diagnostic.RS0027.severity = none
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ bash tests/Sdcb.SimdCodecs.WasmSmoke/run.sh # Mono AOT (
locally after touching the facade or a codec's `IImageDecoder` adapter.
- **No new warnings.** The build stays at its current set (pre-existing XML-doc warnings only); fix
what you introduce, never suppress it.
- **Public API changes are visible, not frozen.** Every package under `src/` lists its public API in
`PublicAPI.Shipped.txt` (what the last release shipped) and `PublicAPI.Unshipped.txt` (changes since); a change
not written there is a build error (RS0016 / RS0017), so every change shows up in review. Changing or removing an
API is allowed when it is worth it (all packages ship together): update the files, and name the break in the
release notes. On a release, Unshipped moves into Shipped.
- **A skipped oracle is not a passing oracle.** Oracle tests skip when their tool is missing; read the
skip messages, or run with `REQUIRE_ORACLES=1`.
- Every vector kernel has a scalar twin and must produce identical output on every tier
Expand Down
9 changes: 9 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@
<NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>
</PropertyGroup>

<!-- Every shipped package (src/*) tracks its public API: a change that is not in PublicAPI.Shipped.txt or
PublicAPI.Unshipped.txt fails the build (RS0016 / RS0017), so every API change shows up in review; changes,
breaking ones included, are fine once written there. On a release, Unshipped moves into Shipped. -->
<ItemGroup Condition="$([System.String]::Copy('$(MSBuildProjectDirectory)').Replace('\', '/').Contains('/src/'))">
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" PrivateAssets="all" />
<AdditionalFiles Include="PublicAPI.Shipped.txt" />
<AdditionalFiles Include="PublicAPI.Unshipped.txt" />
</ItemGroup>

<ItemGroup Condition="Exists('$(MSBuildProjectDirectory)/linqpad-samples')">
<!-- LINQPad shows a package's linqpad-samples folder in its NuGet browser. Each package carries
the samples about itself; the images they use are downloaded at run time (tools/sample-assets). -->
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
</PropertyGroup>

<ItemGroup>
<!-- Shipped packages: the public API baseline (PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt per package). -->
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<!-- Sdcb.SimdCodecs.Tests (xunit v3 + Shouldly). -->
<PackageVersion Include="Shouldly" Version="4.3.0" />
Expand Down
26 changes: 21 additions & 5 deletions docs/api-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

## 保留的原则

- **静态抽象成员 + 显式注册。** codec 不靠反射发现,NativeAOT 和 Mono AOT 都安全。每个 codec 用自己具体类型的方法组
构造 `Registration`(解码器的 `ImageDecoderRegistration`、编码器的 `ImageEncoderRegistration`),门面只引用这些属性,
从不在泛型方法里绑定委托(Mono AOT 会在那里陷入)。wasm 冒烟测试覆盖所有门面入口。
- **静态抽象成员 + 显式注册。** codec 不靠反射发现,NativeAOT 和 Mono AOT 都安全。每个 codec 用对象初始化器、以自己
具体类型的方法组构造 `Registration`(解码器的 `ImageDecoderRegistration`、编码器的 `ImageEncoderRegistration`),门面
只引用这些属性,从不在泛型方法里绑定委托(Mono AOT 会在那里陷入)。wasm 冒烟测试覆盖所有门面入口。
- **`ReadOnlySpan<byte>` 一次性 API 是热路径。** 会话 API(`ImageDecoder`)建在它之上。
- **错误约定。** 不可信输入:`Try*` 返回 `false`(抛出型 API 抛 `InvalidDataException`),不挂起,不无界分配。取消
抛 `OperationCanceledException`;调用方给的选项做不到(不支持的缩放、越界的区域)抛 `NotSupportedException` /
Expand Down Expand Up @@ -51,20 +51,24 @@ Premultiplied,只对 Rgba / Bgra 有意义)。构造函数拒绝无意义的
- **取消**:检查点放在粗粒度位置(JPEG 每个 MCU 行、HEIF 每个 tile 和每 64 行输出、PNG / QOI 等每 64 行、WebP 每个
宏块行),不进内层循环。
- **`ImageInfo`** 在解码前给出 `ColorEncoding`(判断是不是 PQ / HLG)、`Resolution`(单位按文件声明)、`HasThumbnail`。
- **`ImageDecoderFallback`** 给不具备原生能力的 codec 一行实现:解码再转换、原尺寸、无元数据、无缩略图。
- **`ImageDecoderFallback`** 给不具备原生能力的 codec 一行实现:解码再转换、原尺寸、无元数据、无缩略图。它和
`DecodeOptions` 的 `Validate` / `Apply`(扩展成员)、`BufferedEncoderSession` 一样放在
`Sdcb.SimdCodecs.Abstractions.CodecSupport`:只有写 codec 的人需要,用户的 IntelliSense 里看不到。

## 元数据与缩略图

接口层只提取和透传原始块(`ImageMetadata`:Exif 从 TIFF 头开始,XMP 是 UTF-8 包,ICC),解析交给 Exif 包,
Abstractions 不依赖它。提取:JPEG(APP1 Exif / XMP、APP2 ICC)、PNG(eXIf、iTXt XMP、iCCP)、WebP(EXIF、XMP、ICCP)、
TIFF(XMP 标签 700、ICC)、HEIF(`cdsc` 关联的 Exif item 和 `mime` XMP item、`colr` ICC)。缩略图:JPEG Exif IFD1、
HEIF `thmb`。编码器把帧的 `ImageMetadata` 写进各自格式的位置。
HEIF `thmb`。编码器把帧的 `ImageMetadata` 写进各自格式的位置。ICC 只有这一个入口(`IDecodedImage.Metadata.Icc`)。
`ImageMetadata` 是 record,用初始化器构造、`with` 修改,以后加块(IPTC、文本、HDR 静态元数据)不必破坏兼容。

## 会话:`ImageDecoder` / `ImageFrame`

借鉴 WIC:`ImageDecoder.Open(Stream | path | memory)`、`OpenAsync`;`Format`、`FrameCount`、`LoopCount`、`Metadata`;
`GetFrame(i)` 给出 `Info`、`Duration`、`Decode(options)`、`CopyPixels(region, format, buffer, stride)`、
`TryGetClosestSize`、`TryGetThumbnail`。输入经过 `IByteSource`(内存、可寻址流、内存映射文件,不可寻址流先缓冲)。
`IByteSource` 放在 Abstractions 而不是门面里,这样 2.x 的 codec 可以直接接收它、只读需要的部分。
2.0 里所有 codec 仍在整份文件上解码;逐 codec 换成增量输入是 2.x 的性能项,不改 API。

## 平面 YCbCr:`IPlanarImageDecoder`
Expand All @@ -91,3 +95,15 @@ duration)` → `WriteRows(rows, count, stride)` → `Commit()`。
`RasterImage` 只有工厂方法:`Wrap`(接管调用方填好的数组或 `IMemoryOwner`)、`Allocate` / `Rent` / `Create`(创建后
经 `WritablePixels` 填充)、`Crop`。`IDecodedImage : IDisposable`;释放后访问 `Pixels` 抛 `ObjectDisposedException`。
不指定池时行为同 1.x,`Dispose` 可以不调用。

## 兼容性

公开 API 的变动要看得见,但不冻结:所有包同时发布,值得时可以改、可以删,在发布说明里写明即可。形状尽量让常见的
扩展不必破坏兼容:

- 注册类和 `ImageMetadata` 用 `init` 属性(门面需要的标 `required`),不用位置参数的构造函数:加成员不会让已编译的
调用方在运行时找不到方法。只有成对出现的平面委托由 `SimdCodecs.Register` 调 `Validate()` 检查。
- 每个包都有 `PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt`(`Microsoft.CodeAnalysis.PublicApiAnalyzers`)。
Shipped 记的是上一次发布的公开 API;之后的变动写进 Unshipped,不写就编译报错(RS0016 / RS0017),这样每处变动都会
出现在评审的 diff 里。破坏性改动照常可以做,只要它值得、并在发布说明里写明。发布一个版本时把 Unshipped 并进 Shipped。同一分析器里关于可选参数重载形状的 RS0026 / RS0027 在 `.editorconfig` 里关闭:codec 有意
同时提供 `byte[]` 和 `ReadOnlySpan<byte>` 重载并带相同的可选参数。
2 changes: 1 addition & 1 deletion docs/known-gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ HEIF 解码也已调优(M3),基准项目是 `Sdcb.SimdCodecs.Heif.Benchmar
逐 codec 换成真正的增量输入是 2.x 的性能项。
- **`ColorEncoding` 不反映 ICC。** 只有格式自己的色彩标记(PNG 的 cICP,表示线性的 gAMA 1.0,HEIF 的 nclx
`colr` 盒或 HEVC VUI)会写进 `IDecodedImage.ColorEncoding`;只带 ICC 的图片(例如 Display P3 照片)仍报默认的 sRGB。要知道 ICC 声明的
色彩空间,请读 `IccProfile`。
色彩空间,请读 `Metadata.Icc`。
- **EXIF 方向只报告、不应用。** `ImageInfo.Orientation` 告诉你该怎样旋转或翻转,像素本身始终是存储时的
方向。
- **浮点图像没有自动的整数投影。** 浮点 TIFF、增益图 JPEG 重建出的 HDR 等,`TryDecodeInto` 返回 `false`
Expand Down
2 changes: 1 addition & 1 deletion docs/known-gaps/jpeg-gainmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

条目约定(“不支持”“门面不提供”“未调优”)和跨格式的门面限制见 [总览](../known-gaps.md)。

- **重建出的 HDR 不带 ICC。** `IDecodedImage.IccProfile` 有意留空:浮点结果是线性光,底图的 ICC 描述的是
- **重建出的 HDR 不带 ICC。** `IDecodedImage.Metadata.Icc` 有意留空:浮点结果是线性光,底图的 ICC 描述的是
非线性的显示曲线,套上去会误导色彩管理。需要底图的配置文件时,用 `JpegDecoder.TryReadIccProfile(file)` 读取,
并注明它属于 SDR 底图。原色仍会报告:`ColorEncoding.Primaries` 取自底图的 ICC(`IccProfiles.TryGetPrimaries`
能识别 BT.709、Display P3、BT.2020),没有 ICC 时按 sRGB 报 BT.709,识别不了时报 `Unspecified`。
Expand Down
13 changes: 8 additions & 5 deletions docs/migrating-to-2.0.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Migrating to 2.0

2.0 makes all its breaking changes to `Sdcb.SimdCodecs.Abstractions` and the facade at once, so that 2.x can add
without breaking again. Most code needs a rename and nothing else. Every 8-bit decode output is byte-for-byte what
2.0 makes its breaking changes to `Sdcb.SimdCodecs.Abstractions` and the facade at once, and shapes the API so that
the usual additions in 2.x need no further break. Most code needs a rename and nothing else. Every 8-bit decode output is byte-for-byte what
1.x produced; the decoded pixels change only where 2.0 keeps precision 1.x threw away (HEIF Main 10, see below).

## Renames
Expand All @@ -10,10 +10,11 @@ without breaking again. Most code needs a rename and nothing else. Every 8-bit d
|---|---|
| `ImagePixelFormat` (enum) | `PixelFormat` (readonly record struct): `PixelFormat.Rgba32`, `.Bgra32`, `.Rgb24`, `.Bgr24`, `.Gray8` are the same layouts |
| `PixelConverter.BytesPerPixel(format)` | `format.BytesPerPixel` |
| `new RasterImage(w, h, channels, sample, pixels, icc, color)` | `RasterImage.Wrap(w, h, channels, sample, pixels, icc, color)` |
| `new RasterImage(w, h, channels, sample, pixels, icc, color)` | `RasterImage.Wrap(w, h, channels, sample, pixels, color, metadata: new ImageMetadata { Icc = icc })` |
| `image.IccProfile` | `image.Metadata.Icc` (the ICC profile now travels with the Exif and XMP blocks) |
| `rasterImage.ExpandToRgba8(destination)` | `image.TryCopyTo(destination, 0, PixelFormat.Rgba32)` |
| `PixelConverter.ConvertRow(src, channels, sample, dst, ImagePixelFormat, n)` | the same call with a `PixelFormat`; or the new `ConvertRow(src, srcFormat, dst, dstFormat, n)` from any format to any format |
| `new ImageDecoderRegistration(format, canDecode, readInfo, decode, decodeInto[, openFrames])` | use the codec's `XxxImageDecoder.Registration` property; building one by hand takes the eight 2.0 members |
| `new ImageDecoderRegistration(format, canDecode, readInfo, decode, decodeInto[, openFrames])` | use the codec's `XxxImageDecoder.Registration` property; one built by hand is an object initializer: `new ImageDecoderRegistration { Format = …, CanDecode = …, … }` with the eight required 2.0 members |

Code that `switch`ed over `ImagePixelFormat` switches over `format.Layout` (`ChannelLayout.Rgba`, …) or compares with
`==`. A method that took `ImagePixelFormat format = ImagePixelFormat.Rgba32` takes `PixelFormat? format = null`.
Expand All @@ -29,7 +30,9 @@ Code that `switch`ed over `ImagePixelFormat` switches over `format.Layout` (`Cha
value from narrowing the 16-bit image; `TryDecodeInto` to `Rgba64` gives the full precision.
- **New members on interfaces.** A third-party `IImageDecoder` implements `TryGetClosestSize`, `TryReadMetadata`,
`TryDecodeThumbnail` (`ImageDecoderFallback` has one-line implementations for a codec without native support) and
the `DecodeOptions?` / `CancellationToken` parameters of `TryDecode` / `TryDecodeInto`.
the `DecodeOptions?` / `CancellationToken` parameters of `TryDecode` / `TryDecodeInto`. The helpers for codec
authors (`ImageDecoderFallback`, `BufferedEncoderSession`, `DecodeOptions.Validate` / `Apply`) are in
`Sdcb.SimdCodecs.Abstractions.CodecSupport`.
- **Cancellation.** Every decode and encode entry point takes a `CancellationToken`; a cancelled token throws
`OperationCanceledException` (it never makes a `Try` method return false).
- **`ImageInfo` reports more**: `ColorEncoding`, `Resolution`, `HasThumbnail`. Code comparing whole `ImageInfo` values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
using System.IO;
using System.IO.MemoryMappedFiles;

namespace Sdcb.SimdCodecs;
namespace Sdcb.SimdCodecs.Abstractions;

/// <summary>
/// Random-access bytes an image is read from: memory, a seekable stream or a memory-mapped file. The session API
/// (<see cref="ImageDecoder"/>) reads through it, so a codec can later fetch only the parts it needs (a HEIF item,
/// a TIFF strip) instead of the whole file. In 2.0 every codec still decodes from the whole file in memory:
/// <see cref="ImageDecoder"/> materialises it once (<see cref="GetMemory"/>), without a copy for memory and mapped
/// files.
/// Random-access bytes an image is read from: memory, a seekable stream or a memory-mapped file. The facade's session
/// API (<c>Sdcb.SimdCodecs.ImageDecoder</c>) reads through it. It lives here, beside the codec interfaces, so that a
/// codec can later take one and fetch only the parts it needs (a HEIF item, a TIFF strip) instead of the whole file.
/// In 2.0 every codec still decodes from the whole file in memory: the session materialises it once
/// (<see cref="GetMemory"/>), without a copy for memory and mapped files.
/// </summary>
public interface IByteSource : IDisposable
{
Expand All @@ -28,7 +28,7 @@ public interface IByteSource : IDisposable
ReadOnlyMemory<byte> GetMemory();
}

/// <summary>The <see cref="IByteSource"/> implementations the session API opens.</summary>
/// <summary>The <see cref="IByteSource"/> implementations: over memory, a stream, or a file.</summary>
public static class ByteSource
{
/// <summary>A source over memory the caller keeps unchanged while it is used.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Collections.Generic;
using System.Threading;

namespace Sdcb.SimdCodecs.Abstractions;
namespace Sdcb.SimdCodecs.Abstractions.CodecSupport;

/// <summary>One frame a <see cref="BufferedEncoderSession"/> has collected: its pixels, tightly packed, in the format
/// its rows arrived in.</summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;

namespace Sdcb.SimdCodecs.Abstractions.CodecSupport;

/// <summary>
/// What a codec does with the <see cref="DecodeOptions"/> it is given: check them against the image, and apply a
/// region it cannot decode natively. Extension members, so a codec writes <c>options?.Validate(…)</c> and
/// <c>DecodeOptions.Apply(…)</c>; they stay out of a consumer's view unless this namespace is imported.
/// </summary>
public static class DecodeOptionsChecks
{
extension(DecodeOptions options)
{
/// <summary>
/// Checks the options against a stored <paramref name="width"/> × <paramref name="height"/> image, for a
/// decoder that scales natively by <paramref name="supportsScale"/> (or only at 1).
/// </summary>
/// <exception cref="NotSupportedException">The decoder cannot produce <see cref="DecodeOptions.ScaleDenominator"/>.</exception>
/// <exception cref="ArgumentException"><see cref="DecodeOptions.Region"/> does not lie inside the scaled image.</exception>
public void Validate(int width, int height, bool supportsScale)
{
var scale = options.ScaleDenominator;
if (scale != 1 && !supportsScale)
throw new NotSupportedException(
$"This decoder does not scale natively; it cannot decode at 1/{scale}. Ask TryGetClosestSize, then scale the result.");
if (options.Region is { } r)
{
var (w, h) = options.GetScaledSize(width, height);
if (!r.FitsIn(w, h))
throw new ArgumentException($"The region {r} does not lie inside the {w}x{h} image.", nameof(options.Region));
}
}
}

extension(DecodeOptions)
{
/// <summary>
/// The generic region path for a decoder without native region decoding: checks the options (at scale 1
/// only), then crops <paramref name="image"/> — the whole decoded image — to <see cref="DecodeOptions.Region"/>,
/// disposing it. Without a region it returns <paramref name="image"/> itself.
/// </summary>
/// <exception cref="NotSupportedException"><see cref="DecodeOptions.ScaleDenominator"/> is not 1.</exception>
/// <exception cref="ArgumentException"><see cref="DecodeOptions.Region"/> does not lie inside the image.</exception>
public static IDecodedImage Apply(IDecodedImage image, DecodeOptions? options)
{
ArgumentNullException.ThrowIfNull(image);
if (options is null)
return image;
options.Validate(image.Width, image.Height, supportsScale: false);
if (options.Region is not { } r)
return image;
using (image)
return RasterImage.Crop(image, r, options.Pool);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading;

namespace Sdcb.SimdCodecs.Abstractions;
namespace Sdcb.SimdCodecs.Abstractions.CodecSupport;

/// <summary>
/// The generic paths an <see cref="IImageDecoder"/> implementation falls back on for what it has no native code
Expand Down
2 changes: 1 addition & 1 deletion src/Sdcb.SimdCodecs.Abstractions/ColorEncoding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public enum FloatSemantics : byte
/// <see cref="IDecodedImage.SampleFormat"/>). Uses the ITU-T H.273 vocabulary
/// (<see cref="ColorPrimaries"/> / <see cref="TransferFunction"/>) shared with
/// PNG-3 cICP signalling and ICC v4.4 <c>cicp</c> tags. Like
/// <see cref="IDecodedImage.IccProfile"/> this is informational — it never rescales
/// <see cref="ImageMetadata.Icc"/> this is informational — it never rescales
/// pixels; it tells a consumer what transform the samples are already in.
/// <para>
/// There is deliberately no matrix-coefficients slot:
Expand Down
Loading
Loading