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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@

A pure Swift 6.2 implementation of JPEG 2000 (ISO/IEC 15444) encoding and decoding with strict concurrency support.

**Current Version**: 10.17.0
**Status**: Apple Silicon-first JPEG 2000 / HTJ2K (Part-15) implementation. v10.17.0 ships **`J2KDICOMHelpers` (Phase 1)** — a new public SwiftPM library that bridges DICOM Transfer Syntax UIDs to `J2KEncodingConfiguration`. Closes a long-standing product-layer gap: library consumers using their own DICOM parser (DICOMKit, pydicom-via-XPC, etc.) get a Transfer Syntax UID + Pixel Data bytes, and previously had to hand-roll the UID-to-config mapping. Ships `J2KDICOMTransferSyntax` enum (7 DICOM UIDs, round-trip + classification), `J2KDICOMCodestreamDetector.detect(_:)` (SOC + CAP sniff), and `J2KDICOMPhotometricInterpretation` ↔ `J2KColorSpace` mapping. ADR-004 compliant: no DICOM library dependency anywhere; new product depends only on J2KCore + J2KCodec; consumers not importing it are unaffected. MINOR / additive product — no perf change, codestream bytes byte-identical to v10.16.0.
**Previous Release**: 10.16.0 (JP3DDecoder partial-decode discoverable convenience overloads)
**Current Version**: 10.18.0
**Status**: Apple Silicon-first JPEG 2000 / HTJ2K (Part-15) implementation. v10.18.0 ships **JP3D AsyncSequence progress reporting** — `progressStream()` extensions on `JP3DDecoder` / `JP3DROIDecoder` / `JP3DEncoder` / `JP3DStreamWriter`, modern Swift-concurrency progress API alongside the existing `setProgressCallback(_:)` closure surface. Closes the deferred Phase 3 from v10.17.0's plan with a focused MINOR scope so the AsyncStream lifecycle (race-free setup via `async` method, documented overwrite semantics) is implemented cleanly. Consumers gain idiomatic `for await progress in decoder.progressStream() { … }` iteration without hand-rolling a `setProgressCallback` → `AsyncStream.makeStream` adapter. MINOR / additive — pure surface, no existing API change, codestream bytes byte-identical to v10.17.0.
**Previous Release**: 10.17.0 (J2KDICOMHelpers Phase 1 — new SwiftPM product)
**Release process**: see [RELEASING.md](RELEASING.md). Every release MUST update this README (Current Version line + new Release Status paragraph) — see the Release artefacts checklist for the full requirements.

## 📦 Release Status

**v10.18.0** ships **JP3D AsyncSequence progress reporting** — `progressStream()` extensions on `JP3DDecoder`, `JP3DROIDecoder`, `JP3DEncoder`, and `JP3DStreamWriter`. Modern Swift concurrency progress API alongside the existing `setProgressCallback(_:)` closure surface (both remain supported indefinitely). Consumers using structured concurrency previously had to hand-roll a `setProgressCallback` → `AsyncStream.makeStream` adapter themselves; v10.18.0 ships that adapter inside the JP3D module so it composes cleanly with the existing async-await encode/decode methods. The `progressStream()` method is `async` so the relay closure is installed on the actor BEFORE the stream is returned — no race window where early progress events get missed. Lifecycle behaviour is explicitly codified: calling `progressStream()` twice overwrites the first stream's writer (the first stream's continuation receives no further events but isn't explicitly finished); the stream doesn't auto-finish at operation completion (consumers `break` based on observed progress or rely on `Task` cancellation); `setProgressCallback(_:)` after `progressStream()` overwrites the relay (use one or the other, not both). **Validation**: `V10_30_ProgressStreamParityTests` **4/4 PASS** release mode — JP3DDecoder + JP3DEncoder both deliver progress events during real operations; JP3DROIDecoder stream surface is available + safe to consume (its decoder body doesn't currently fire `progressCallback` events — pre-existing upstream gap, documented in the test header); `setProgressCallback` overwrites of `progressStream`'s relay still fire correctly. Full `swift test --filter JP3D` regression sweep **532/532 PASS** (528 pre-existing + 4 new V10_30 + 1 pre-existing skip); mandatory commit gate 7/7 PASS. Closes the deferred Phase 3 from v10.17.0's plan with focused MINOR scope. Also bumps `getVersion()` 10.17.0 → 10.18.0. Codestream bytes byte-identical to v10.17.0. See [RELEASE_NOTES_v10.18.0.md](RELEASE_NOTES_v10.18.0.md).

**v10.17.0** ships **`J2KDICOMHelpers` (Phase 1)** — a new public SwiftPM library closing a long-standing product-layer gap. Per [ADR-004](Documentation/ADR/ADR-004-no-dicom-dependency.md), J2KSwift core libraries don't depend on any DICOM library. The CLI ships a `DICOMSupport.swift` for `.dcm` file loading (uses a Python fallback for compressed transfer syntaxes), but the `DICOMTransferSyntax` / `DICOMTransferSyntaxInfo` types there are CLI-private — not surfaced to library consumers. Library consumers using their own DICOM parser (DICOMKit, pydicom-via-XPC, etc.) extract `(Transfer Syntax UID, Pixel Data bytes)` and previously had to hand-roll the UID-to-`J2KEncodingConfiguration` mapping. v10.17.0 lifts the UID-bridge layer into a new public `J2KDICOMHelpers` SwiftPM library: **`J2KDICOMTransferSyntax`** enum (`CaseIterable` over all seven DICOM Part 5 Annex A JPEG 2000 / HTJ2K UIDs — `j2kLossless`, `j2kLossy`, `j2kPart2MulticompLossless`, `j2kPart2Multicomp`, `htj2kLossless`, `htj2kLossyConstant`, `htj2kLossy`) with `init?(uid:)` / `var uid` round-trip (whitespace + trailing-NUL tolerant per PS3.5 §6.2), `isLossless` / `isHTJ2K` / `isPart2` classification flags, and `encodingConfiguration(bitDepth:psnrTarget:)` returning a matching `J2KEncodingConfiguration` (with `htj2kBlockFormat: .conformant` for DICOM interop); **`J2KDICOMCodestreamDetector.detect(_:)`** sniffs SOC + (optional) CAP markers in the first ~256 bytes to classify raw codestreams as `.j2kLossless` (Part 1, no CAP) or `.htj2kLossless` (Part 15, CAP present), returning nil for non-J2K bytes (JPEG SOI, PNG signature, random bytes all correctly reject); and **`J2KDICOMPhotometricInterpretation`** enum mirror (`MONOCHROME1/2`, `RGB`, `YBR_FULL/422`, `YBR_RCT/ICT`) with bidirectional `J2KColorSpace` mapping (greyscale ↔ MONOCHROME2, sRGB ↔ RGB, yCbCr ↔ YBR_FULL; `hdr`/`hdrLinear`/`iccProfile`/`unknown` return nil reverse). ADR-004 compliant: **no DICOM library dependency anywhere in J2KSwift** — the new product depends only on J2KCore + J2KCodec; consumers not importing `J2KDICOMHelpers` are completely unaffected. **Validation**: `V10_29_*` test suites **26/26 PASS** release mode (9 TransferSyntaxRoundTrip + 5 EncodingConfigurationParity + 6 CodestreamDetection + 6 PhotometricInterpretation); end-to-end bit-exact lossless round-trip confirmed for `.j2kLossless` + `.htj2kLossless` via the helpers-built configurations. Full `swift test --filter JP3D` regression sweep **528/528 PASS**; mandatory commit gate 7/7 PASS. Also bumps `getVersion()` 10.16.0 → 10.17.0. Phase 1 deliberately ships the smallest viable shape — UID-and-config bridge only, no DICOM file parsing (Phase 2 territory: extract more of `J2KCLI/DICOMSupport.swift` or ship a DICOMKit adapter as a sibling opt-in product). AsyncSequence progress streams (originally Phase 3) deferred to a dedicated MINOR. See [RELEASE_NOTES_v10.17.0.md](RELEASE_NOTES_v10.17.0.md).

**v10.16.0** adds three **convenience overloads on `JP3DDecoder`** that surface the JP3D partial-decode capabilities behind the canonical type. The underlying pipelines have been production-default since v10.10.0 (true partial-resolution from v10.18-research, K+ROI composition from v10.13.0), but the only way to reach them from `import J2K3D` was to either construct `JP3DDecoderConfiguration(resolutionLevel: K)` (discoverable only by reading docs) or switch to `JP3DROIDecoder` (a separate actor type). v10.16.0 closes the gap with `decode(_:resolutionLevel:)`, `decode(_:region:)`, and `decode(_:region:resolutionLevel:)` overloads — all thin wrappers that construct a transient inner decoder/ROI-decoder with the requested options and delegate. The actor's `configuration` (`tolerateErrors`, `maxQualityLayers`) propagates to the transient inner instances; negative levels are clamped to 0; level 0 is bit-exact equivalent to `decode(_:)`. **Speedups surfaced (existing pipelines, v10.18-research bench)**: level-1 thumbnail decode **2.2-3.1× faster** than full decode on small-mid JP3D fixtures (mr_3d_small 2.22×, ct_3d_small 3.05×, mr_3d_mid 3.01×); ROI ~1/4-extent decode **3.4-4.1× faster** (mr_3d_small 3.37×, ct_3d_small 4.14×, mr_3d_mid 3.96×); K+ROI composition stacks both savings. Decoder-only, codestream bytes byte-identical to v10.15.0, encoder unchanged. **Validation**: `V10_28_JP3DDecoderPartialOverloadsParityTests` **6/6 PASS** (all three overloads byte-identical to existing JP3DDecoderConfiguration + JP3DROIDecoder usage; no-op level (0) bit-exact to `decode(_:)`; negative level clamped; outer-configuration propagation verified); full `swift test --filter JP3D` regression sweep **528/528 PASS** (519 pre-existing + 9 new = 2 V10_27 probe + 6 V10_28 parity + 1 pre-existing skip); mandatory commit gate 7/7 PASS. The natural symmetric ship to v10.15.0 would have been `JP3DEncoder.preWarm`; the V10_27 cold-start probe killed that candidate (leftover encoder-specific cold cost +0.10 / −1.25 ms on 128/256-cube fixtures — `JP3DDecoder.preWarm(includeWarmupDispatch: true)` already amortises encoder Metal init via `J2KMetalSession.processShared`). Also bumps the stale `getVersion()` constant `10.9.2 → 10.16.0` (drifted across 8 releases). See [RELEASE_NOTES_v10.16.0.md](RELEASE_NOTES_v10.16.0.md).
Expand Down
217 changes: 217 additions & 0 deletions RELEASE_NOTES_v10.18.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# J2KSwift v10.18.0

**JP3D AsyncSequence progress reporting — `progressStream()` extensions on
`JP3DDecoder` / `JP3DROIDecoder` / `JP3DEncoder` / `JP3DStreamWriter`.**
Modern Swift concurrency progress API alongside the existing
`setProgressCallback(_:)` closure surface (which remains supported).
Closes the deferred Phase 3 from v10.17.0's plan with a focused MINOR
scope so the AsyncStream lifecycle is implemented cleanly.

MINOR per RELEASING.md — pure additive surface: 4 new public extensions,
zero existing API change, codestream bytes byte-identical to v10.17.0,
no perf change on warm or cold paths.

## Summary

Consumers using Swift's structured-concurrency `for await ... in stream`
pattern previously had to wrap `setProgressCallback(_:)` themselves in
an `AsyncStream.makeStream(of:)` adapter. v10.18.0 ships that adapter
inside the JP3D module so it composes cleanly with the existing
async-await encode/decode methods:

```swift
import J2K3D

let decoder = JP3DDecoder()
let progressStream = await decoder.progressStream()

// Spawn a child task to drive progress UI updates:
async let progressDisplay: Void = {
for await progress in progressStream {
print("\(progress.stage): \(Int(progress.overallProgress * 100))%")
}
}()

// Decode produces progress events that flow through the stream:
let result = try await decoder.decode(data)
_ = await progressDisplay // join the progress task
```

The relay closure is installed on the actor BEFORE `progressStream()`
returns (the method is `async`), so there's no race window where early
progress events get missed.

## What's New — production-default

| Public API | v10.17.0 | v10.18.0 |
|---|---|---|
| `JP3DDecoder.progressStream()` | _not present_ | **NEW** — `async`, returns `AsyncStream<JP3DDecoderProgress>` |
| `JP3DROIDecoder.progressStream()` | _not present_ | **NEW** — `async`, returns `AsyncStream<JP3DDecoderProgress>` (surface available; upstream decoder body doesn't currently fire progress events — see Known limitations) |
| `JP3DEncoder.progressStream()` | _not present_ | **NEW** — `async`, returns `AsyncStream<JP3DEncoderProgress>` |
| `JP3DStreamWriter.progressStream()` | _not present_ | **NEW** — `async`, returns `AsyncStream<JP3DStreamProgress>` |
| `setProgressCallback(_:)` on all of the above | unchanged | unchanged |
| `getVersion()` | 10.17.0 | 10.18.0 |
| Every other public API | unchanged | unchanged |

The new extensions install their stream-relay closure as the actor's
progress callback. If a prior `setProgressCallback(_:)` closure was
registered, `progressStream()` overwrites it. Calling `progressStream()`
a second time also overwrites the first stream's writer; the first
stream's continuation receives no further events but isn't explicitly
finished (subsequent `yield` calls are no-ops on a finished continuation).

## Backward compatibility

- **Codestream bytes**: byte-identical to v10.17.0 on every input.
Encoder unchanged.
- **Existing libraries**: zero behaviour change. The four `setProgressCallback`
methods continue to work exactly as before — consumers using closures
are unaffected.
- **API surface**: additive only. Four new extension methods, no
existing signatures changed.

## Why this is a focused MINOR

v10.17.0's original plan included AsyncSequence progress streams as
"Phase 3". They were cut from that release per scope discipline
because the AsyncStream-vs-actor-stored-callback lifecycle had
correctness questions that warranted dedicated attention:

1. **Setup race**: if `progressStream()` were synchronous and the
callback installation deferred to a `Task`, the consumer could
start iterating before the relay was installed and early progress
events would be missed. v10.18.0's `async` method installs the
closure on the actor before returning the stream — race-free.

2. **Termination cleanup**: when the consumer stops iterating (loop
ends, task cancellation), `AsyncStream.Continuation.yield(_:)` on
a finished continuation is a no-op. The actor's stored closure
remains but its yields are silently dropped. The next call to
`progressStream()` or `setProgressCallback(_:)` overwrites the
closure. This is documented in the extension header.

3. **Concurrent streams**: a second `progressStream()` call overwrites
the first stream's writer; the first stream's continuation isn't
explicitly finished but stops receiving events. Documented.

These properties are now codified in V10_30 parity tests.

## Test Suite Results

| Suite | Tests | Result | Coverage |
|---|---:|---|---|
| `V10_30_ProgressStreamParityTests` | 4/4 | PASS | JP3DDecoder + JP3DEncoder both deliver progress events during real operations; JP3DROIDecoder stream surface is available + safe (upstream body doesn't currently fire events — documented); setProgressCallback overwriting progressStream's relay still fires |
| `swift test --filter JP3D` (full regression) | 532/532 | PASS | 528 pre-existing + 4 new V10_30 + 1 pre-existing skip |
| Mandatory commit gate (release mode) | 7/7 | PASS | `J2KMedicalCorpusEncodePerformanceTests` 2/2 + `J2KMedicalCorpusPerformanceTests` 2/2 + `J2KStrictCrossCodecValidationTests` 3/3 |

## API surface — additions only

```swift
extension JP3DDecoder {
/// v10.18.0 — modern AsyncSequence progress reporting.
public func progressStream() async -> AsyncStream<JP3DDecoderProgress>
}

extension JP3DROIDecoder {
/// v10.18.0 — modern AsyncSequence progress reporting.
public func progressStream() async -> AsyncStream<JP3DDecoderProgress>
}

extension JP3DEncoder {
/// v10.18.0 — modern AsyncSequence progress reporting.
public func progressStream() async -> AsyncStream<JP3DEncoderProgress>
}

extension JP3DStreamWriter {
/// v10.18.0 — modern AsyncSequence progress reporting.
public func progressStream() async -> AsyncStream<JP3DStreamProgress>
}
```

No removals. No existing signatures changed. The four `setProgressCallback`
closure-based APIs continue to exist alongside.

## Recommended usage

```swift
import J2K3D

let decoder = JP3DDecoder()

// Set up the stream BEFORE the operation that fires progress.
let stream = await decoder.progressStream()

// Spawn a sibling task to drive the UI / log progress events.
async let observer: Void = {
for await progress in stream {
// Update UI on the main actor as needed
await MainActor.run {
updateProgressBar(progress.overallProgress)
}
}
}()

// The decode call fires progress events as it runs.
let result = try await decoder.decode(data)
_ = await observer
```

## Known limitations

- **JP3DROIDecoder progress events**: the surface
`JP3DROIDecoder.progressStream()` is available and safe to consume,
but the underlying decoder body does NOT currently invoke its stored
`progressCallback` during a region decode (pre-existing upstream
limitation at `Sources/J2K3D/JP3DROIDecoder.swift` — the storage at
line 67 exists but no call sites fire). The V10_30 parity test
documents this as the surface contract. When a later release wires up
ROI-decoder progress reporting, the stream will start delivering
events automatically — no API change needed here.

- **Stream finishing**: the `progressStream()` extension intentionally
does NOT auto-finish the stream when the underlying operation
completes. This keeps the extension free of operation-completion
inspection (the decoder doesn't expose a "decode finished" event
separate from `decode(_:)`'s return). Consumers should `break` out
of their `for await` loop based on observed progress, or rely on
`Task` cancellation to terminate iteration.

- **Two-stream concurrency**: calling `progressStream()` twice on the
same actor instance overwrites the first stream's writer with the
second's; the first stream stops receiving events but isn't
explicitly finished. A consumer model with multiple observers should
fan out from a single stream (`stream.share()` via a custom
`AsyncSequence` adaptor, etc.) rather than calling `progressStream()`
twice.

## Reproducing the test numbers

```bash
swift test -c release --filter "V10_30_ProgressStreamParityTests"
```

Four tests across JP3DDecoder + JP3DROIDecoder + JP3DEncoder + the
overwrite-by-setProgressCallback regression check — all PASS in ~0.3 s
release mode.

## Backward upgrade

`swift package update` won't auto-pick this release if your `Package.swift`
pins an exact version; bump the requirement to `from: "10.18.0"`. No
source changes required for consumers — the new `progressStream()`
methods are strictly additive. Code that uses `setProgressCallback(_:)`
continues to work without modification.

## Companion — Next release candidates

After v10.18.0 ships, the listed candidates from v10.17.0's planning
remain:
1. **J2KDICOMHelpers Phase 2** — DICOM file parser extraction from
`J2KCLI/DICOMSupport.swift` into the helpers product; potentially
with a DICOMKit / pydicom-via-XPC adapter as an opt-in sibling
product.
2. **JPIP Phase 1 response parser** — 2-3 weeks, closes the
notImplemented-across-all-public-methods state of the JPIP module.
3. **IncrementalJ2KDecoder completion** — 2-3 weeks, closes a
notImplemented stub at
`Sources/J2KCodec/J2KAdvancedDecoding.swift:430`.
Loading
Loading