diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6a2e4a8..7a6956d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -35,7 +35,7 @@ validation, string manipulation, and mathematical operations. - When code changes are made, the following documents **must** be updated: - **README.md**: Update to reflect any new features, API changes, or usage instructions. - - **MILESTONES.md**: Update to reflect progress on project milestones and goals. + - **docs/MILESTONES.md**: Update to reflect progress on project milestones and goals. - All public types and methods must have documentation comments using `///` syntax. - Include usage examples in documentation comments where helpful. @@ -52,8 +52,12 @@ JLSwift/ │ ├── copilot-instructions.md # This file │ └── workflows/ │ └── ci.yml # CI pipeline with coverage enforcement -├── README.md # Project documentation -└── MILESTONES.md # Project milestones and roadmap +├── README.md # Project overview (repo home) +├── CHANGELOG.md # Release history +├── docs/ # Guides, specs, and reference docs +│ └── MILESTONES.md # Project milestones and roadmap (+ others) +└── man/ + └── jpegls.1 # CLI man page ``` ## Build and Test Commands @@ -96,7 +100,7 @@ Before submitting or approving a PR, verify: 1. All tests pass (`swift test`). 2. Test coverage is above 95%. 3. README.md is updated if features or APIs changed. -4. MILESTONES.md is updated if milestone progress changed. +4. docs/MILESTONES.md is updated if milestone progress changed. 5. All public APIs have documentation comments. 6. Code follows Swift 6.2+ best practices and concurrency model. 7. Automatic code review has been run and feedback addressed. diff --git a/.gitignore b/.gitignore index ff94bcd..a849f92 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ *.xcworkspace/ Package.resolved default.profraw + +# Stray compiler intermediates (emitted to CWD by older broken builds) +*.o +*.d +*.dia +*.swiftdeps +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 15b762d..dc27742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - USAGE_EXAMPLES.md with 25+ real-world standalone examples - SWIFTUI_EXAMPLES.md with SwiftUI integration patterns for iOS/macOS - APPKIT_EXAMPLES.md with AppKit integration patterns for macOS -- Man page documentation (jpegls.1) in groff format +- Man page documentation (man/jpegls.1) in groff format - Man page installation instructions in README ### Changed @@ -300,7 +300,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Release Notes Format -See [RELEASE_NOTES_TEMPLATE.md](RELEASE_NOTES_TEMPLATE.md) for the release notes template used for GitHub releases. +See [RELEASE_NOTES_TEMPLATE.md](docs/RELEASE_NOTES_TEMPLATE.md) for the release notes template used for GitHub releases. ## Version History diff --git a/Package.swift b/Package.swift index 16365d8..7c8307f 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( ), .executable( name: "jpegls", - targets: ["jpegls"] + targets: ["jpeglscli"] ), ], dependencies: [ @@ -32,7 +32,7 @@ let package = Package( ] ), .executableTarget( - name: "jpegls", + name: "jpeglscli", dependencies: [ "JPEGLS", .product(name: "ArgumentParser", package: "swift-argument-parser"), diff --git a/README.md b/README.md index ee8766e..4fc20eb 100644 --- a/README.md +++ b/README.md @@ -543,12 +543,12 @@ Complete documentation for the `jpegls` command-line tool is available as a Unix ```bash # System-wide installation (requires sudo) -sudo cp jpegls.1 /usr/local/share/man/man1/ +sudo cp man/jpegls.1 /usr/local/share/man/man1/ sudo mandb # Update man page database (on Linux) # User installation (no sudo required) mkdir -p ~/.local/share/man/man1 -cp jpegls.1 ~/.local/share/man/man1/ +cp man/jpegls.1 ~/.local/share/man/man1/ # Add to ~/.bashrc or ~/.zshrc: export MANPATH="$HOME/.local/share/man:$MANPATH" ``` @@ -559,7 +559,7 @@ cp jpegls.1 ~/.local/share/man/man1/ man jpegls # Or view directly without installation -man ./jpegls.1 +man ./man/jpegls.1 ``` The man page includes: @@ -577,7 +577,7 @@ The man page includes: | 1.2.840.10008.1.2.4.80 | JPEG-LS Lossless Image Compression | | 1.2.840.10008.1.2.4.81 | JPEG-LS Lossy (Near-Lossless) Image Compression | -See [MILESTONES.md](MILESTONES.md) for the detailed development roadmap. +See [MILESTONES.md](docs/MILESTONES.md) for the detailed development roadmap. ## Building & Testing @@ -714,7 +714,7 @@ JLSwift includes comprehensive conformance testing using reference files from th The conformance tests are located in `Tests/JPEGLSTests/CharLSConformanceTests.swift` with reference fixtures in `Tests/JPEGLSTests/TestFixtures/`. These tests ensure compatibility with the JPEG-LS standard (ISO/IEC 14495-1:1999 / ITU-T.87) and provide a foundation for bit-exact comparison with CharLS output. -A full standards conformance matrix (`CONFORMANCE_MATRIX.md`) documents the mapping between every normative section of ITU-T.87 and its implementation in JLSwift, including all deviations found and fixed during Milestone 10 (Phases 10.1–10.4). +A full standards conformance matrix (`docs/CONFORMANCE_MATRIX.md`) documents the mapping between every normative section of ITU-T.87 and its implementation in JLSwift, including all deviations found and fixed during Milestone 10 (Phases 10.1–10.4). ### Code Coverage Requirement @@ -740,7 +740,11 @@ JLSwift/ │ └── workflows/ │ └── ci.yml # CI pipeline (build → test with caching & concurrency) ├── README.md # This file -└── MILESTONES.md # Project roadmap +├── CHANGELOG.md # Release history +├── docs/ # Guides, specs, and reference docs (see docs/README.md) +│ └── MILESTONES.md # Project roadmap (+ guides, specs) +└── man/ + └── jpegls.1 # CLI man page ``` ## Documentation @@ -748,19 +752,19 @@ JLSwift/ | Document | Description | |----------|-------------| | [README.md](README.md) | Project overview and usage guide (this file) | -| [GETTING_STARTED.md](GETTING_STARTED.md) | Quick start guide with examples and common patterns | -| [USAGE_EXAMPLES.md](USAGE_EXAMPLES.md) | Comprehensive real-world usage examples | -| [SWIFTUI_EXAMPLES.md](SWIFTUI_EXAMPLES.md) | SwiftUI integration guide with image loading examples | -| [APPKIT_EXAMPLES.md](APPKIT_EXAMPLES.md) | AppKit integration guide for macOS applications | -| [SERVER_SIDE_EXAMPLES.md](SERVER_SIDE_EXAMPLES.md) | Server-side Swift integration guide (Vapor, Hummingbird, NIO) | -| [DICOMKIT_INTEGRATION.md](DICOMKIT_INTEGRATION.md) | DICOMkit integration guide for DICOM imaging workflows | -| [PERFORMANCE_TUNING.md](PERFORMANCE_TUNING.md) | Performance optimisation and benchmarking guide | -| [METAL_GPU_ACCELERATION.md](METAL_GPU_ACCELERATION.md) | Metal GPU acceleration guide for large images | -| [TROUBLESHOOTING.md](TROUBLESHOOTING.md) | Common issues and solutions | -| [VERSIONING.md](VERSIONING.md) | Semantic versioning strategy and release guidelines | +| [GETTING_STARTED.md](docs/GETTING_STARTED.md) | Quick start guide with examples and common patterns | +| [USAGE_EXAMPLES.md](docs/USAGE_EXAMPLES.md) | Comprehensive real-world usage examples | +| [SWIFTUI_EXAMPLES.md](docs/SWIFTUI_EXAMPLES.md) | SwiftUI integration guide with image loading examples | +| [APPKIT_EXAMPLES.md](docs/APPKIT_EXAMPLES.md) | AppKit integration guide for macOS applications | +| [SERVER_SIDE_EXAMPLES.md](docs/SERVER_SIDE_EXAMPLES.md) | Server-side Swift integration guide (Vapor, Hummingbird, NIO) | +| [DICOMKIT_INTEGRATION.md](docs/DICOMKIT_INTEGRATION.md) | DICOMkit integration guide for DICOM imaging workflows | +| [PERFORMANCE_TUNING.md](docs/PERFORMANCE_TUNING.md) | Performance optimisation and benchmarking guide | +| [METAL_GPU_ACCELERATION.md](docs/METAL_GPU_ACCELERATION.md) | Metal GPU acceleration guide for large images | +| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common issues and solutions | +| [VERSIONING.md](docs/VERSIONING.md) | Semantic versioning strategy and release guidelines | | [CHANGELOG.md](CHANGELOG.md) | Complete history of changes and releases | -| [MILESTONES.md](MILESTONES.md) | Project milestones and development roadmap | -| [X86_64_REMOVAL_GUIDE.md](X86_64_REMOVAL_GUIDE.md) | Step-by-step guide for removing x86-64 support | +| [MILESTONES.md](docs/MILESTONES.md) | Project milestones and development roadmap | +| [X86_64_REMOVAL_GUIDE.md](docs/X86_64_REMOVAL_GUIDE.md) | Step-by-step guide for removing x86-64 support | | [Copilot Instructions](.github/copilot-instructions.md) | Coding guidelines for contributors | ### API Documentation @@ -774,9 +778,9 @@ swift package generate-documentation ### User Guides -- **[Getting Started](GETTING_STARTED.md)**: Installation, quick start, and basic usage examples -- **[Performance Tuning](PERFORMANCE_TUNING.md)**: Hardware acceleration, memory optimisation, and profiling -- **[Troubleshooting](TROUBLESHOOTING.md)**: Solutions to common problems and debugging tips +- **[Getting Started](docs/GETTING_STARTED.md)**: Installation, quick start, and basic usage examples +- **[Performance Tuning](docs/PERFORMANCE_TUNING.md)**: Hardware acceleration, memory optimisation, and profiling +- **[Troubleshooting](docs/TROUBLESHOOTING.md)**: Solutions to common problems and debugging tips ## Contributing @@ -785,7 +789,7 @@ When contributing to JLSwift, please follow these guidelines: 1. **Code Style**: Follow the [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) 2. **Testing**: All public APIs must have corresponding unit tests with >95% coverage 3. **Documentation**: All public types and methods must have documentation comments -4. **Documentation Updates**: Update README.md and MILESTONES.md when features or APIs change +4. **Documentation Updates**: Update README.md and docs/MILESTONES.md when features or APIs change 5. **British English**: All comments, documentation, help text, and error messages must use British English (e.g., colour, optimise, initialise, behaviour, organisation). Public API identifiers (type names, method names, property names) retain their original American spelling. ### Pull Request Checklist @@ -793,7 +797,7 @@ When contributing to JLSwift, please follow these guidelines: - [ ] All tests pass (`swift test`) - [ ] Test coverage is above 95% - [ ] README.md is updated if features or APIs changed -- [ ] MILESTONES.md is updated if milestone progress changed +- [ ] docs/MILESTONES.md is updated if milestone progress changed - [ ] All public APIs have documentation comments - [ ] Code follows Swift 6.2+ best practices diff --git a/Sources/jpeglscli/BenchDICOMCommand.swift b/Sources/jpeglscli/BenchDICOMCommand.swift new file mode 100644 index 0000000..841ce4f --- /dev/null +++ b/Sources/jpeglscli/BenchDICOMCommand.swift @@ -0,0 +1,245 @@ +import ArgumentParser +import Foundation +import JPEGLS + +extension JPEGLSCLITool { + /// Walk a tree of DICOM files, encode each native grayscale frame to + /// JPEG-LS, decode it back, verify the round-trip is bit-exact, and report + /// compression ratio and throughput grouped by modality. + /// + /// Intended for validating the codec against real medical-imaging data + /// (the radiology DICOM corpus). It is both a conformance check + /// (lossless == every reconstructed sample matches the original) and a + /// performance baseline (MB/s encode / decode per modality). + struct BenchDICOM: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "bench-dicom", + abstract: "Round-trip & benchmark JPEG-LS over a DICOM image corpus", + discussion: """ + Recursively scans a directory for .dcm files, groups them by the + top-level folder (treated as the modality, e.g. CT/DX/MG/MR/PX/US/XA), + and for every native (uncompressed) unsigned grayscale frame: + + encode (lossless) → decode → verify bit-exact → record ratio + timing + + Non-encodable frames (encapsulated/compressed, signed, or colour) + are counted and skipped. Results are aggregated per modality. + + Examples: + jpegls bench-dicom "/path/to/Radiology DICOM Data" + jpegls bench-dicom CORPUS --limit 5 + jpegls bench-dicom CORPUS --modality CT,DX --max-pixels 4000000 + jpegls bench-dicom CORPUS --limit 10 --json + """ + ) + + @Argument(help: "Root directory to scan recursively for .dcm files") + var root: String + + @Option(name: .long, help: "Max frames to process per modality (0 = no limit; default 3)") + var limit: Int = 3 + + @Option(name: .long, help: "Skip frames larger than this many pixels (0 = no cap)") + var maxPixels: Int = 0 + + @Option(name: .long, help: "Comma-separated modality filter (e.g. CT,DX); default: all") + var modality: String? + + @Option(name: .long, help: "Near-lossless parameter (0 = lossless; default 0)") + var near: Int = 0 + + @Flag(name: .long, help: "Emit machine-readable JSON instead of a table") + var json: Bool = false + + // MARK: - Per-frame / per-modality accumulators + + struct FrameResult { + var modality: String + var width: Int + var height: Int + var bitsStored: Int + var originalBytes: Int + var compressedBytes: Int + var encodeSeconds: Double + var decodeSeconds: Double + var lossless: Bool + } + + struct ModalityStats { + var processed = 0 // encoded + decoded + var skipped = 0 // parsed, but not a native unsigned grayscale frame + var errors = 0 // could not read / parse the DICOM file at all + var failures = 0 // encoded but the round-trip was NOT bit-exact + var originalBytes = 0 + var compressedBytes = 0 + var encodeSeconds = 0.0 + var decodeSeconds = 0.0 + var pixels = 0 + } + + mutating func run() throws { + let rootURL = URL(fileURLWithPath: root, isDirectory: true) + var isDir: ObjCBool = false + guard FileManager.default.fileExists(atPath: rootURL.path, isDirectory: &isDir), isDir.boolValue else { + throw ValidationError("Not a directory: \(root)") + } + + let modalityFilter: Set? = modality.map { + Set($0.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }) + } + + // Collect .dcm paths grouped by modality (first path component under root). + var byModality: [String: [URL]] = [:] + let rootComponents = rootURL.standardizedFileURL.pathComponents + guard let walker = FileManager.default.enumerator( + at: rootURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) else { + throw ValidationError("Cannot enumerate \(root)") + } + for case let url as URL in walker { + guard url.pathExtension.lowercased() == "dcm" else { continue } + let comps = url.standardizedFileURL.pathComponents + let mod = comps.count > rootComponents.count ? comps[rootComponents.count] : "(root)" + if let f = modalityFilter, !f.contains(mod) { continue } + byModality[mod, default: []].append(url) + } + + if byModality.isEmpty { + throw ValidationError("No .dcm files found under \(root)") + } + + var stats: [String: ModalityStats] = [:] + var frames: [FrameResult] = [] + let encoder = JPEGLSEncoder() + let decoder = JPEGLSDecoder() + + for mod in byModality.keys.sorted() { + let urls = byModality[mod]!.sorted { $0.path < $1.path } + var s = ModalityStats() + if !json { FileHandle.standardError.write(Data("Scanning \(mod) (\(urls.count) files)…\n".utf8)) } + + for url in urls { + if limit > 0 && s.processed >= limit { break } + + guard let data = try? Data(contentsOf: url), + let img = try? DICOMSupport.parse(data) else { + s.errors += 1 + continue + } + + guard let (pixels, bps) = DICOMSupport.grayscaleFrame(img) else { + s.skipped += 1 + continue + } + + let pixelCount = img.columns * img.rows + if maxPixels > 0 && pixelCount > maxPixels { + s.skipped += 1 + continue + } + + do { + let imageData = try MultiComponentImageData.grayscale(pixels: pixels, bitsPerSample: bps) + let config = try JPEGLSEncoder.Configuration(near: near) + + let t0 = DispatchTime.now().uptimeNanoseconds + let encoded = try encoder.encode(imageData, configuration: config) + let t1 = DispatchTime.now().uptimeNanoseconds + let decoded = try decoder.decode(encoded) + let t2 = DispatchTime.now().uptimeNanoseconds + + let lossless = (near == 0) && pixelsEqual(decoded.components.first?.pixels, pixels) + let originalBytes = img.columns * img.rows * img.bytesPerSample + let encS = Double(t1 - t0) / 1e9 + let decS = Double(t2 - t1) / 1e9 + + s.processed += 1 + s.originalBytes += originalBytes + s.compressedBytes += encoded.count + s.encodeSeconds += encS + s.decodeSeconds += decS + s.pixels += pixelCount + if near == 0 && !lossless { s.failures += 1 } + + frames.append(FrameResult( + modality: mod, width: img.columns, height: img.rows, + bitsStored: img.bitsStored, originalBytes: originalBytes, + compressedBytes: encoded.count, encodeSeconds: encS, + decodeSeconds: decS, lossless: lossless + )) + } catch { + s.failures += 1 + } + } + stats[mod] = s + } + + if json { + printJSON(stats: stats, frames: frames) + } else { + printTable(stats: stats) + } + } + + // MARK: - Helpers + + private func pixelsEqual(_ a: [[Int]]?, _ b: [[Int]]) -> Bool { + guard let a = a, a.count == b.count else { return false } + for r in 0.. 0 { + print("⚠️ \(t.failures) frame(s) failed the lossless round-trip.") + } else if t.processed > 0 { + print("✓ All \(t.processed) processed frame(s) round-tripped losslessly.") + } + print("(skip = non-grayscale/encapsulated frames; err = unreadable DICOM)") + } + + private func printRow(_ mod: String, _ s: ModalityStats) { + let ratio = s.compressedBytes > 0 ? Double(s.originalBytes) / Double(s.compressedBytes) : 0 + let encMBs = s.encodeSeconds > 0 ? (Double(s.originalBytes) / 1_000_000.0) / s.encodeSeconds : 0 + let decMBs = s.decodeSeconds > 0 ? (Double(s.originalBytes) / 1_000_000.0) / s.decodeSeconds : 0 + let mp = Double(s.pixels) / 1_000_000.0 + print(String(format: "%-8@ %7d %6d %6d %6d %8.2f:1 %8.1f %10.1f %10.1f", + mod as NSString, s.processed, s.skipped, s.errors, s.failures, ratio, mp, encMBs, decMBs)) + } + + private func printJSON(stats: [String: ModalityStats], frames: [FrameResult]) { + var modObjs: [String] = [] + for mod in stats.keys.sorted() { + let s = stats[mod]! + let ratio = s.compressedBytes > 0 ? Double(s.originalBytes) / Double(s.compressedBytes) : 0 + modObjs.append(""" + {"modality":"\(mod)","frames":\(s.processed),"skipped":\(s.skipped),"errors":\(s.errors),"failures":\(s.failures),"originalBytes":\(s.originalBytes),"compressedBytes":\(s.compressedBytes),"ratio":\(String(format: "%.4f", ratio)),"encodeSeconds":\(String(format: "%.4f", s.encodeSeconds)),"decodeSeconds":\(String(format: "%.4f", s.decodeSeconds))} + """) + } + print("{\"near\":\(near),\"modalities\":[\(modObjs.joined(separator: ","))]}") + } + } +} diff --git a/Sources/jpeglscli/DICOMSupport.swift b/Sources/jpeglscli/DICOMSupport.swift new file mode 100644 index 0000000..9ceb7bd --- /dev/null +++ b/Sources/jpeglscli/DICOMSupport.swift @@ -0,0 +1,313 @@ +import Foundation +import JPEGLS + +/// Minimal DICOM Part-10 reader, scoped to what the JPEG-LS test/benchmark +/// harness needs: image geometry, pixel format, and the Pixel Data element. +/// +/// This deliberately lives in the CLI target — the `JPEGLS` library is, by +/// design, DICOM-independent. The parser understands Explicit and Implicit VR +/// Little Endian datasets (the meta group is always Explicit VR LE) and both +/// native (uncompressed) and encapsulated Pixel Data. It does **not** aim to be +/// a general DICOM toolkit: it reads only the handful of tags required to drive +/// a lossless round-trip, and skips everything else (including sequences). +/// +/// Known limitation: a small number of multi-frame Ultrasound files use +/// Implicit VR LE datasets with deeply nested private sequences that defeat +/// purely sequential parsing; `parse` throws for those. Callers (e.g. the +/// `bench-dicom` harness) treat such files as unreadable and skip them rather +/// than failing — they are not a JPEG-LS conformance signal. +enum DICOMSupport { + + // MARK: - Public model + + /// A decoded DICOM image header plus its raw Pixel Data bytes. + struct Image: Sendable { + var rows: Int // (0028,0010) + var columns: Int // (0028,0011) + var samplesPerPixel: Int // (0028,0002), default 1 + var bitsAllocated: Int // (0028,0100) + var bitsStored: Int // (0028,0101) + var highBit: Int // (0028,0102) + var pixelRepresentation: Int // (0028,0103): 0 = unsigned, 1 = signed + var planarConfiguration: Int // (0028,0006): 0 = interleaved, 1 = planar + var numberOfFrames: Int // (0028,0008), default 1 + var photometric: String // (0028,0004) + var transferSyntaxUID: String // (0002,0010) + var isEncapsulated: Bool + /// Native pixel bytes (uncompressed), or the concatenated codestream + /// fragments (encapsulated). + var pixelData: Data + + var isSigned: Bool { pixelRepresentation == 1 } + var bytesPerSample: Int { (bitsAllocated + 7) / 8 } + + /// Whether this frame can be fed to the JPEG-LS encoder for a lossless + /// round-trip: native (uncompressed), unsigned, ≤16-bit samples. + var isEncodableLossless: Bool { + !isEncapsulated && !isSigned && bitsAllocated <= 16 && bitsStored >= 1 + && rows > 0 && columns > 0 && samplesPerPixel >= 1 + } + } + + enum DICOMError: Error, CustomStringConvertible { + case notDICOM + case truncated + case missingPixelData + case unsupported(String) + + var description: String { + switch self { + case .notDICOM: return "not a DICOM Part-10 file (missing 'DICM' magic)" + case .truncated: return "DICOM stream ended unexpectedly" + case .missingPixelData: return "no Pixel Data (7FE0,0010) element found" + case .unsupported(let s): return "unsupported DICOM feature: \(s)" + } + } + } + + // Well-known transfer syntaxes. + static let implicitVRLittleEndian = "1.2.840.10008.1.2" + static let explicitVRLittleEndian = "1.2.840.10008.1.2.1" + + // VRs that carry a 32-bit length (with 2 reserved bytes) in Explicit VR. + private static let longFormVRs: Set = { + let strs = ["OB", "OW", "OF", "OD", "OL", "OV", "SQ", "UC", "UR", "UT", "UN"] + return Set(strs.map { vrCode($0) }) + }() + + private static func vrCode(_ s: String) -> UInt16 { + let b = Array(s.utf8) + return UInt16(b[0]) | (UInt16(b[1]) << 8) + } + + // MARK: - Parse + + static func parse(_ data: Data) throws -> Image { + let bytes = [UInt8](data) + // Part-10 preamble: 128-byte preamble + "DICM". + guard bytes.count > 132, + bytes[128] == 0x44, bytes[129] == 0x49, + bytes[130] == 0x43, bytes[131] == 0x4D else { + throw DICOMError.notDICOM + } + + var cursor = 132 + + // --- File Meta Information (group 0002): always Explicit VR LE. --- + var transferSyntax = implicitVRLittleEndian + while cursor + 8 <= bytes.count { + let group = readU16(bytes, cursor) + if group != 0x0002 { break } // end of meta group + let element = readU16(bytes, cursor + 2) + let (length, valueOffset) = readExplicitVRHeader(bytes, cursor) + if group == 0x0002, element == 0x0010 { + transferSyntax = readString(bytes, valueOffset, length) + } + cursor = valueOffset + Int(length) + } + + // Every transfer syntax *except* Implicit VR LE encodes the dataset in + // Explicit VR Little Endian — including the encapsulated JPEG / JPEG-LS + // / RLE syntaxes. Only "1.2.840.10008.1.2" is implicit. + let explicit = (transferSyntax != implicitVRLittleEndian) + // Anything that isn't one of the two native LE syntaxes is encapsulated + // (JPEG / JPEG-LS / RLE …). Encapsulated Pixel Data has undefined length. + let encapsulated = !(transferSyntax == implicitVRLittleEndian || transferSyntax == explicitVRLittleEndian) + + // --- Main dataset. --- + var img = Image( + rows: 0, columns: 0, samplesPerPixel: 1, + bitsAllocated: 0, bitsStored: 0, highBit: 0, + pixelRepresentation: 0, planarConfiguration: 0, numberOfFrames: 1, + photometric: "", transferSyntaxUID: transferSyntax, + isEncapsulated: encapsulated, pixelData: Data() + ) + + var sawPixelData = false + while cursor + 8 <= bytes.count { + let group = readU16(bytes, cursor) + let element = readU16(bytes, cursor + 2) + + let length: UInt32 + let valueOffset: Int + if explicit { + (length, valueOffset) = readExplicitVRHeader(bytes, cursor) + } else { + length = readU32(bytes, cursor + 4) + valueOffset = cursor + 8 + } + + // Pixel Data. + if group == 0x7FE0, element == 0x0010 { + if length == 0xFFFF_FFFF { + // Encapsulated: items = Basic Offset Table then fragments. + img.pixelData = readEncapsulatedFragments(bytes, valueOffset) + img.isEncapsulated = true + } else { + let end = min(valueOffset + Int(length), bytes.count) + img.pixelData = Data(bytes[valueOffset.. 0x7FE0 { break } + } + + guard sawPixelData else { throw DICOMError.missingPixelData } + return img + } + + // MARK: - Pixel extraction + + /// Build a single-component `[row][col]` integer array from the first frame + /// of a native, unsigned grayscale image, plus the bit depth to encode at. + /// + /// Returns `nil` if the image isn't a native unsigned grayscale frame. + static func grayscaleFrame(_ img: Image) -> (pixels: [[Int]], bitsPerSample: Int)? { + guard img.isEncodableLossless, img.samplesPerPixel == 1 else { return nil } + + let w = img.columns, h = img.rows + let bps = img.bytesPerSample + let frameBytes = w * h * bps + guard img.pixelData.count >= frameBytes else { return nil } + + var pixels = [[Int]](repeating: [Int](repeating: 0, count: w), count: h) + var maxVal = 0 + // Mask to the stored bits so any padding in the high bits is ignored. + let storedMask = img.bitsStored >= 16 ? 0xFFFF : ((1 << img.bitsStored) - 1) + + img.pixelData.withUnsafeBytes { (raw: UnsafeRawBufferPointer) in + let base = raw.baseAddress! + for row in 0.. maxVal { maxVal = value } + } + } + } + + // JPEG-LS bit depth must cover the actual sample range, and be ≥ 2. + var bits = max(2, img.bitsStored) + while (1 << bits) - 1 < maxVal && bits < 16 { bits += 1 } + return (pixels, bits) + } + + // MARK: - Low-level readers + + private static func readU16(_ b: [UInt8], _ i: Int) -> UInt16 { + UInt16(b[i]) | (UInt16(b[i + 1]) << 8) + } + + private static func readU32(_ b: [UInt8], _ i: Int) -> UInt32 { + UInt32(b[i]) | (UInt32(b[i + 1]) << 8) | (UInt32(b[i + 2]) << 16) | (UInt32(b[i + 3]) << 24) + } + + private static func readString(_ b: [UInt8], _ off: Int, _ length: UInt32) -> String { + let end = min(off + Int(length), b.count) + guard off <= end else { return "" } + let slice = b[off.. (UInt32, Int) { + let vr = readU16(b, start + 4) + if longFormVRs.contains(vr) { + // VR(2) + reserved(2) + length(4) + let length = readU32(b, start + 8) + return (length, start + 12) + } else { + // VR(2) + length(2) + let length = UInt32(readU16(b, start + 6)) + return (length, start + 8) + } + } + + /// Given the offset just past an undefined-length element header, skip the + /// entire sequence (handling nested items / sequences) and return the offset + /// immediately following its Sequence Delimitation Item (FFFE,E0DD). + private static func skipUndefinedLengthSequence(_ b: [UInt8], _ start: Int) -> Int { + var i = start + var depth = 1 + while i + 8 <= b.count && depth > 0 { + let group = readU16(b, i) + let element = readU16(b, i + 2) + let length = readU32(b, i + 4) + i += 8 + if group == 0xFFFE && element == 0xE0DD { // sequence delimiter + depth -= 1 + } else if group == 0xFFFE && element == 0xE000 { // item + if length == 0xFFFF_FFFF { + depth += 1 // undefined-length item: descend + } else { + i += Int(length) // defined-length item: skip + } + } else if length != 0xFFFF_FFFF { + i += Int(length) + } else { + depth += 1 + } + } + return i + } + + /// Read encapsulated Pixel Data fragments: skip the Basic Offset Table item, + /// then concatenate all fragment items up to the Sequence Delimitation Item. + private static func readEncapsulatedFragments(_ b: [UInt8], _ start: Int) -> Data { + var i = start + var out = Data() + var first = true + while i + 8 <= b.count { + let group = readU16(b, i) + let element = readU16(b, i + 2) + let length = readU32(b, i + 4) + i += 8 + if group == 0xFFFE && element == 0xE0DD { break } // sequence delimiter + guard group == 0xFFFE && element == 0xE000 else { break } + let end = min(i + Int(length), b.count) + if first { + first = false // Basic Offset Table — skip its contents + } else { + out.append(contentsOf: b[i.. --help' for detailed help on any command. """, version: "0.1.0", - subcommands: [Encode.self, Decode.self, Info.self, Verify.self, Batch.self, Compare.self, Convert.self, Benchmark.self, Completion.self], + subcommands: [Encode.self, Decode.self, Info.self, Verify.self, Batch.self, Compare.self, Convert.self, Benchmark.self, BenchDICOM.self, Completion.self], defaultSubcommand: nil ) } diff --git a/Tests/JPEGLSTests/GPUComputePhase15ExtendedTests.swift b/Tests/JPEGLSTests/GPUComputePhase15ExtendedTests.swift index 07a1f38..a5017db 100644 --- a/Tests/JPEGLSTests/GPUComputePhase15ExtendedTests.swift +++ b/Tests/JPEGLSTests/GPUComputePhase15ExtendedTests.swift @@ -496,7 +496,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: encodingPipeline — small batch lossless (NEAR=0)") func testEncodingPipelineSmallBatchLossless() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let a: [Int32] = [100, 200, 50] let b: [Int32] = [110, 190, 60] @@ -513,7 +513,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: encodingPipeline — small batch near-lossless (NEAR=3)") func testEncodingPipelineSmallBatchNearLossless() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let a: [Int32] = [100, 200, 50] let b: [Int32] = [110, 190, 60] @@ -535,7 +535,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: encodingPipeline — NEAR=3 gradients near zero → q=0") func testEncodingPipelineNear3GradientsNearZero() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() // b[i]-c[i] = 1, a[i]-c[i] = 0, c[i]-a[i] = 0 → all within NEAR=3 → q=0 // d1 = b - c = 11 - 10 = 1 (≤ NEAR=3) @@ -555,7 +555,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: encodingPipeline — quantised gradients match NEAR=0 for large gradients") func testEncodingPipelineQuantisedGradientsLarge() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() // With NEAR=0, the encoding pipeline should produce same quantisation // as the standalone quantizeGradientsBatch call. @@ -577,7 +577,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: encodingPipeline — empty arrays") func testEncodingPipelineEmpty() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let (pred, err, q1, q2, q3) = try accelerator.computeEncodingPipelineBatch( a: [], b: [], c: [], x: [], near: 0, t1: 3, t2: 7, t3: 21) @@ -588,7 +588,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: decodingPipeline — reconstructs pixels from errors (small batch)") func testDecodingPipelineSmallBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let a: [Int32] = [100, 200, 50] let b: [Int32] = [110, 190, 60] @@ -609,7 +609,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: decodingPipeline — empty arrays") func testDecodingPipelineEmpty() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let result = try accelerator.computeDecodingPipelineBatch( a: [], b: [], c: [], errval: []) @@ -620,7 +620,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: encode → decode round-trip (lossless, small batch)") func testEncodeDecodeLosslessRoundTripSmall() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let a: [Int32] = [100, 200, 50, 150] let b: [Int32] = [110, 190, 60, 140] @@ -636,7 +636,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: encode → decode round-trip (lossless, large batch, GPU path)") func testEncodeDecodeLosslessRoundTripLarge() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = MetalAccelerator.gpuThreshold * 2 var a = [Int32](repeating: 0, count: count) @@ -658,7 +658,7 @@ struct MetalEncodingDecodingPipelineTests { @Test("Metal: encoding pipeline matches standalone gradient + MED + quantise calls") func testEncodingPipelineMatchesStandaloneOps() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = MetalAccelerator.gpuThreshold * 2 var a = [Int32](repeating: 0, count: count) diff --git a/Tests/JPEGLSTests/GPUComputePhase15Tests.swift b/Tests/JPEGLSTests/GPUComputePhase15Tests.swift index 5953af4..86e6b45 100644 --- a/Tests/JPEGLSTests/GPUComputePhase15Tests.swift +++ b/Tests/JPEGLSTests/GPUComputePhase15Tests.swift @@ -448,7 +448,7 @@ struct MetalPhase15Tests { @Test("Metal: quantizeGradientsBatch — small batch (CPU fallback)") func testQuantizeGradientsSmallBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let t1: Int32 = 3, t2: Int32 = 7, t3: Int32 = 21 let d: [Int32] = [-30, -10, -5, -1, 0, 1, 5, 10, 30] @@ -462,7 +462,7 @@ struct MetalPhase15Tests { @Test("Metal: quantizeGradientsBatch — large batch (GPU)") func testQuantizeGradientsLargeBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let t1: Int32 = 3, t2: Int32 = 7, t3: Int32 = 21 let count = MetalAccelerator.gpuThreshold * 2 @@ -478,7 +478,7 @@ struct MetalPhase15Tests { @Test("Metal: quantizeGradientsBatch — empty arrays") func testQuantizeGradientsEmpty() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let (q1, q2, q3) = try accelerator.quantizeGradientsBatch( d1: [], d2: [], d3: [], t1: 3, t2: 7, t3: 21) @@ -487,7 +487,7 @@ struct MetalPhase15Tests { @Test("Metal: quantizeGradientsBatch matches Vulkan CPU fallback") func testQuantizeGradientsBitExact() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let metalAcc = try MetalAccelerator() let vulkanAcc = VulkanAccelerator() let t1: Int32 = 3, t2: Int32 = 7, t3: Int32 = 21 @@ -513,7 +513,7 @@ struct MetalPhase15Tests { @Test("Metal: HP1 forward transform — small batch (CPU fallback)") func testHP1ForwardSmallBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let r: [Int32] = [200, 100] let g: [Int32] = [100, 50] @@ -527,7 +527,7 @@ struct MetalPhase15Tests { @Test("Metal: HP1 inverse round-trips — small batch (CPU fallback)") func testHP1RoundTripSmallBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let r: [Int32] = [200, 100, 50] let g: [Int32] = [100, 50, 30] @@ -541,7 +541,7 @@ struct MetalPhase15Tests { @Test("Metal: HP1 forward transform — large batch (GPU)") func testHP1ForwardLargeBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = MetalAccelerator.gpuThreshold * 2 let r = [Int32](repeating: 200, count: count) @@ -556,7 +556,7 @@ struct MetalPhase15Tests { @Test("Metal: HP1 large batch round-trips correctly (GPU)") func testHP1RoundTripLargeBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = MetalAccelerator.gpuThreshold * 2 var r = [Int32](repeating: 0, count: count) @@ -578,7 +578,7 @@ struct MetalPhase15Tests { @Test("Metal: HP2 inverse round-trips correctly — large batch (GPU)") func testHP2RoundTripLargeBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = MetalAccelerator.gpuThreshold * 2 var r = [Int32](repeating: 0, count: count) @@ -600,7 +600,7 @@ struct MetalPhase15Tests { @Test("Metal: HP3 inverse round-trips correctly — large batch (GPU)") func testHP3RoundTripLargeBatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = MetalAccelerator.gpuThreshold * 2 var r = [Int32](repeating: 0, count: count) @@ -622,7 +622,7 @@ struct MetalPhase15Tests { @Test("Metal: .none forward transform returns input unchanged") func testIdentityForward() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let r: [Int32] = [10, 20, 30] let g: [Int32] = [40, 50, 60] @@ -636,7 +636,7 @@ struct MetalPhase15Tests { @Test("Metal colour transforms match Vulkan CPU results — HP1 large batch") func testColourTransformBitExactHP1() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let metalAcc = try MetalAccelerator() let vulkanAcc = VulkanAccelerator() let count = MetalAccelerator.gpuThreshold * 2 @@ -657,7 +657,7 @@ struct MetalPhase15Tests { @Test("Metal colour transforms match Vulkan CPU results — HP2 large batch") func testColourTransformBitExactHP2() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let metalAcc = try MetalAccelerator() let vulkanAcc = VulkanAccelerator() let count = MetalAccelerator.gpuThreshold * 2 @@ -678,7 +678,7 @@ struct MetalPhase15Tests { @Test("Metal colour transforms match Vulkan CPU results — HP3 large batch") func testColourTransformBitExactHP3() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let metalAcc = try MetalAccelerator() let vulkanAcc = VulkanAccelerator() let count = MetalAccelerator.gpuThreshold * 2 diff --git a/Tests/JPEGLSTests/JPEGLSRoundTripInteroperabilityTests.swift b/Tests/JPEGLSTests/JPEGLSRoundTripInteroperabilityTests.swift index 485ee63..da0c2b5 100644 --- a/Tests/JPEGLSTests/JPEGLSRoundTripInteroperabilityTests.swift +++ b/Tests/JPEGLSTests/JPEGLSRoundTripInteroperabilityTests.swift @@ -12,7 +12,7 @@ // // Note: Near-lossless encoder round-trip tests are limited to small images (≤8×8) // due to a known pre-existing encoder issue. The decoder is correct (validated by -// CharLS bit-exact comparison tests). See Phase 8.1 / Phase 12.1 in MILESTONES.md. +// CharLS bit-exact comparison tests). See Phase 8.1 / Phase 12.1 in docs/MILESTONES.md. // // Note: Test patterns avoid purely flat (constant-value) images >8×8 and gradient // patterns that produce flat transformed components after HP1/HP3 colour transforms, diff --git a/Tests/JPEGLSTests/MetalAcceleratorTests.swift b/Tests/JPEGLSTests/MetalAcceleratorTests.swift index 576d755..7c58340 100644 --- a/Tests/JPEGLSTests/MetalAcceleratorTests.swift +++ b/Tests/JPEGLSTests/MetalAcceleratorTests.swift @@ -34,7 +34,7 @@ struct MetalAcceleratorTests { @Test("Compute gradients for single pixel batch") func testSinglePixelGradients() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -56,7 +56,7 @@ struct MetalAcceleratorTests { @Test("Compute gradients for small batch (CPU fallback)") func testSmallBatchGradients() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -76,7 +76,7 @@ struct MetalAcceleratorTests { @Test("Compute gradients for large batch (GPU)") func testLargeBatchGradients() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -105,7 +105,7 @@ struct MetalAcceleratorTests { @Test("Compute gradients with negative values") func testGradientsWithNegativeValues() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -125,7 +125,7 @@ struct MetalAcceleratorTests { @Test("Compute gradients with boundary values") func testGradientsWithBoundaryValues() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -146,7 +146,7 @@ struct MetalAcceleratorTests { @Test("Compute gradients with empty arrays") func testGradientsWithEmptyArrays() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -165,7 +165,7 @@ struct MetalAcceleratorTests { @Test("Compute MED prediction for single pixel") func testSinglePixelMEDPrediction() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -193,7 +193,7 @@ struct MetalAcceleratorTests { @Test("Compute MED prediction for small batch (CPU fallback)") func testSmallBatchMEDPrediction() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -211,7 +211,7 @@ struct MetalAcceleratorTests { @Test("Compute MED prediction for large batch (GPU)") func testLargeBatchMEDPrediction() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -267,7 +267,7 @@ struct MetalAcceleratorTests { @Test("Compute MED prediction with equal pixel values") func testMEDPredictionWithEqualValues() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -283,7 +283,7 @@ struct MetalAcceleratorTests { @Test("Compute MED prediction with zero values") func testMEDPredictionWithZeroValues() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -298,7 +298,7 @@ struct MetalAcceleratorTests { @Test("Compute MED prediction with empty arrays") func testMEDPredictionWithEmptyArrays() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() @@ -315,7 +315,7 @@ struct MetalAcceleratorTests { @Test("Metal gradients match scalar implementation") func testGradientsBitExactMatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let metalAccelerator = try MetalAccelerator() let scalarAccelerator = ScalarAccelerator() @@ -354,7 +354,7 @@ struct MetalAcceleratorTests { @Test("Metal MED predictions match scalar implementation") func testMEDPredictionBitExactMatch() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let metalAccelerator = try MetalAccelerator() let scalarAccelerator = ScalarAccelerator() diff --git a/Tests/JPEGLSTests/MetalPerformanceBenchmarks.swift b/Tests/JPEGLSTests/MetalPerformanceBenchmarks.swift index ac08f0e..7f4f06d 100644 --- a/Tests/JPEGLSTests/MetalPerformanceBenchmarks.swift +++ b/Tests/JPEGLSTests/MetalPerformanceBenchmarks.swift @@ -60,7 +60,7 @@ struct MetalPerformanceBenchmarks { @Test("Benchmark: Metal gradient computation - 512×512 (below threshold)") func benchmarkGradients512x512() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = 512 * 512 // 262,144 pixels (below GPU threshold) @@ -72,7 +72,7 @@ struct MetalPerformanceBenchmarks { } // Measure - let (minTime, maxTime, avgTime) = measure { + let (minTime, maxTime, avgTime) = try measure { _ = try accelerator.computeGradientsBatch(a: a, b: b, c: c) } @@ -93,7 +93,7 @@ struct MetalPerformanceBenchmarks { @Test("Benchmark: Metal gradient computation - 1024×1024 (at threshold)") func benchmarkGradients1024x1024() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = 1024 * 1024 // 1,048,576 pixels (just above GPU threshold) @@ -105,7 +105,7 @@ struct MetalPerformanceBenchmarks { } // Measure - let (minTime, maxTime, avgTime) = measure { + let (minTime, maxTime, avgTime) = try measure { _ = try accelerator.computeGradientsBatch(a: a, b: b, c: c) } @@ -126,7 +126,7 @@ struct MetalPerformanceBenchmarks { @Test("Benchmark: Metal gradient computation - 2048×2048 (large image)") func benchmarkGradients2048x2048() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = 2048 * 2048 // 4,194,304 pixels @@ -138,7 +138,7 @@ struct MetalPerformanceBenchmarks { } // Measure - let (minTime, maxTime, avgTime) = measure { + let (minTime, maxTime, avgTime) = try measure { _ = try accelerator.computeGradientsBatch(a: a, b: b, c: c) } @@ -161,7 +161,7 @@ struct MetalPerformanceBenchmarks { @Test("Benchmark: Metal gradient computation - 4096×4096 (very large)") func benchmarkGradients4096x4096() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = 4096 * 4096 // 16,777,216 pixels @@ -173,7 +173,7 @@ struct MetalPerformanceBenchmarks { } // Measure - let (minTime, maxTime, avgTime) = measure { + let (minTime, maxTime, avgTime) = try measure { _ = try accelerator.computeGradientsBatch(a: a, b: b, c: c) } @@ -198,7 +198,7 @@ struct MetalPerformanceBenchmarks { @Test("Benchmark: Metal MED prediction - 1024×1024") func benchmarkMEDPrediction1024x1024() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = 1024 * 1024 @@ -210,7 +210,7 @@ struct MetalPerformanceBenchmarks { } // Measure - let (minTime, maxTime, avgTime) = measure { + let (minTime, maxTime, avgTime) = try measure { _ = try accelerator.computeMEDPredictionBatch(a: a, b: b, c: c) } @@ -230,7 +230,7 @@ struct MetalPerformanceBenchmarks { @Test("Benchmark: Metal MED prediction - 2048×2048") func benchmarkMEDPrediction2048x2048() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let accelerator = try MetalAccelerator() let count = 2048 * 2048 @@ -242,7 +242,7 @@ struct MetalPerformanceBenchmarks { } // Measure - let (minTime, maxTime, avgTime) = measure { + let (minTime, maxTime, avgTime) = try measure { _ = try accelerator.computeMEDPredictionBatch(a: a, b: b, c: c) } @@ -264,7 +264,7 @@ struct MetalPerformanceBenchmarks { @Test("Benchmark: CPU vs GPU comparison - 2048×2048 gradients") func benchmarkCPUvsGPUGradients() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let metalAccelerator = try MetalAccelerator() let scalarAccelerator = ScalarAccelerator() @@ -276,7 +276,7 @@ struct MetalPerformanceBenchmarks { _ = try metalAccelerator.computeGradientsBatch(a: a, b: b, c: c) } - let (_, _, gpuTime) = measure { + let (_, _, gpuTime) = try measure { _ = try metalAccelerator.computeGradientsBatch(a: a, b: b, c: c) } @@ -311,7 +311,7 @@ struct MetalPerformanceBenchmarks { @Test("Benchmark: CPU vs GPU comparison - 2048×2048 MED prediction") func benchmarkCPUvsGPUMEDPrediction() throws { - #guard(MetalAccelerator.isSupported) + guard MetalAccelerator.isSupported else { return } let metalAccelerator = try MetalAccelerator() let scalarAccelerator = ScalarAccelerator() @@ -323,7 +323,7 @@ struct MetalPerformanceBenchmarks { _ = try metalAccelerator.computeMEDPredictionBatch(a: a, b: b, c: c) } - let (_, _, gpuTime) = measure { + let (_, _, gpuTime) = try measure { _ = try metalAccelerator.computeMEDPredictionBatch(a: a, b: b, c: c) } diff --git a/Tests/JPEGLSTests/Phase16OptimisationTests.swift b/Tests/JPEGLSTests/Phase16OptimisationTests.swift index 875bbfc..5719a1e 100644 --- a/Tests/JPEGLSTests/Phase16OptimisationTests.swift +++ b/Tests/JPEGLSTests/Phase16OptimisationTests.swift @@ -7,7 +7,7 @@ import Foundation /// Validates that the Milestone 16 performance improvements produce bit-exact /// output equivalent to the reference implementations and report measurable /// speed gains. Tests cover: -/// - Phase 16.1: Baseline measurements (documented in MILESTONES.md) +/// - Phase 16.1: Baseline measurements (documented in docs/MILESTONES.md) /// - Phase 16.2: Algorithmic optimisations (CLZ Golomb-Rice, gradient table) /// - Phase 16.3: Memory & I/O optimisations (batch bit writes, buffer pre-sizing) diff --git a/default.profraw b/default.profraw deleted file mode 100644 index 4c6737d..0000000 Binary files a/default.profraw and /dev/null differ diff --git a/APPKIT_EXAMPLES.md b/docs/APPKIT_EXAMPLES.md similarity index 100% rename from APPKIT_EXAMPLES.md rename to docs/APPKIT_EXAMPLES.md diff --git a/CONFORMANCE_MATRIX.md b/docs/CONFORMANCE_MATRIX.md similarity index 100% rename from CONFORMANCE_MATRIX.md rename to docs/CONFORMANCE_MATRIX.md diff --git a/DECODER_BUG_INVESTIGATION.md b/docs/DECODER_BUG_INVESTIGATION.md similarity index 100% rename from DECODER_BUG_INVESTIGATION.md rename to docs/DECODER_BUG_INVESTIGATION.md diff --git a/DECODER_IMPLEMENTATION_SPEC.md b/docs/DECODER_IMPLEMENTATION_SPEC.md similarity index 100% rename from DECODER_IMPLEMENTATION_SPEC.md rename to docs/DECODER_IMPLEMENTATION_SPEC.md diff --git a/DICOMKIT_INTEGRATION.md b/docs/DICOMKIT_INTEGRATION.md similarity index 100% rename from DICOMKIT_INTEGRATION.md rename to docs/DICOMKIT_INTEGRATION.md diff --git a/GETTING_STARTED.md b/docs/GETTING_STARTED.md similarity index 98% rename from GETTING_STARTED.md rename to docs/GETTING_STARTED.md index 495a6f2..84462ce 100644 --- a/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -418,7 +418,7 @@ do { ### Learn More -- **[README.md](README.md)**: Detailed feature documentation +- **[README.md](../README.md)**: Detailed feature documentation - **[USAGE_EXAMPLES.md](USAGE_EXAMPLES.md)**: Comprehensive real-world usage examples - **[MILESTONES.md](MILESTONES.md)**: Development roadmap and progress - **[PERFORMANCE_TUNING.md](PERFORMANCE_TUNING.md)**: Performance optimisation guide @@ -458,7 +458,7 @@ swift test --filter JPEGLSPerformanceBenchmarks ### Community - **Issues**: [GitHub Issues](https://github.com/Raster-Lab/JLSwift/issues) -- **Contributing**: See [Copilot Instructions](.github/copilot-instructions.md) +- **Contributing**: See [Copilot Instructions](../.github/copilot-instructions.md) ### Related Documentation diff --git a/METAL_GPU_ACCELERATION.md b/docs/METAL_GPU_ACCELERATION.md similarity index 100% rename from METAL_GPU_ACCELERATION.md rename to docs/METAL_GPU_ACCELERATION.md diff --git a/MILESTONES.md b/docs/MILESTONES.md similarity index 100% rename from MILESTONES.md rename to docs/MILESTONES.md diff --git a/PERFORMANCE_TUNING.md b/docs/PERFORMANCE_TUNING.md similarity index 100% rename from PERFORMANCE_TUNING.md rename to docs/PERFORMANCE_TUNING.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..cd48b8f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,31 @@ +# JLSwift Documentation + +Documentation for the JLSwift JPEG-LS codec. For the project overview, see the +[root README](../README.md); for release history, see the [CHANGELOG](../CHANGELOG.md). + +## Getting started & usage +- [GETTING_STARTED.md](GETTING_STARTED.md) — install, build, and first encode/decode +- [USAGE_EXAMPLES.md](USAGE_EXAMPLES.md) — real-world standalone usage +- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) — common issues and fixes + +## Platform integration +- [SWIFTUI_EXAMPLES.md](SWIFTUI_EXAMPLES.md) — SwiftUI patterns for iOS/macOS +- [APPKIT_EXAMPLES.md](APPKIT_EXAMPLES.md) — AppKit patterns for macOS +- [SERVER_SIDE_EXAMPLES.md](SERVER_SIDE_EXAMPLES.md) — server-side Swift usage +- [DICOMKIT_INTEGRATION.md](DICOMKIT_INTEGRATION.md) — DICOM/medical-imaging integration + +## Performance & acceleration +- [PERFORMANCE_TUNING.md](PERFORMANCE_TUNING.md) — tuning guide +- [METAL_GPU_ACCELERATION.md](METAL_GPU_ACCELERATION.md) — Metal GPU path +- [VULKAN_GPU_ACCELERATION.md](VULKAN_GPU_ACCELERATION.md) — Vulkan GPU path +- [X86_64_REMOVAL_GUIDE.md](X86_64_REMOVAL_GUIDE.md) — Apple-only consolidation notes + +## Standards & internals +- [CONFORMANCE_MATRIX.md](CONFORMANCE_MATRIX.md) — ITU-T.87 conformance mapping +- [DECODER_IMPLEMENTATION_SPEC.md](DECODER_IMPLEMENTATION_SPEC.md) — decoder design +- [DECODER_BUG_INVESTIGATION.md](DECODER_BUG_INVESTIGATION.md) — decoder debugging notes + +## Project & releases +- [MILESTONES.md](MILESTONES.md) — milestones and roadmap +- [VERSIONING.md](VERSIONING.md) — versioning policy +- [RELEASE_NOTES_TEMPLATE.md](RELEASE_NOTES_TEMPLATE.md) — release-notes template diff --git a/RELEASE_NOTES_TEMPLATE.md b/docs/RELEASE_NOTES_TEMPLATE.md similarity index 98% rename from RELEASE_NOTES_TEMPLATE.md rename to docs/RELEASE_NOTES_TEMPLATE.md index ce53807..09a0c75 100644 --- a/RELEASE_NOTES_TEMPLATE.md +++ b/docs/RELEASE_NOTES_TEMPLATE.md @@ -175,7 +175,7 @@ Thank you to everyone who contributed to this release: ## Full Changelog -See [CHANGELOG.md](CHANGELOG.md) for the complete list of changes in this release. +See [CHANGELOG.md](../CHANGELOG.md) for the complete list of changes in this release. **Full Changelog**: vX.Y.Z-1...vX.Y.Z diff --git a/SERVER_SIDE_EXAMPLES.md b/docs/SERVER_SIDE_EXAMPLES.md similarity index 99% rename from SERVER_SIDE_EXAMPLES.md rename to docs/SERVER_SIDE_EXAMPLES.md index 3f94784..daa6eed 100644 --- a/SERVER_SIDE_EXAMPLES.md +++ b/docs/SERVER_SIDE_EXAMPLES.md @@ -1612,4 +1612,4 @@ For more information: - [USAGE_EXAMPLES.md](USAGE_EXAMPLES.md) - General usage patterns - [PERFORMANCE_TUNING.md](PERFORMANCE_TUNING.md) - Performance optimisation guide - [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Common issues and solutions -- [README.md](README.md) - Project overview and API reference +- [README.md](../README.md) - Project overview and API reference diff --git a/SWIFTUI_EXAMPLES.md b/docs/SWIFTUI_EXAMPLES.md similarity index 100% rename from SWIFTUI_EXAMPLES.md rename to docs/SWIFTUI_EXAMPLES.md diff --git a/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md similarity index 99% rename from TROUBLESHOOTING.md rename to docs/TROUBLESHOOTING.md index de3abbf..500e243 100644 --- a/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -649,7 +649,7 @@ let testData = try MultiComponentImageData.grayscale( ### Useful Resources -- **[README.md](README.md)**: Project overview +- **[README.md](../README.md)**: Project overview - **[GETTING_STARTED.md](GETTING_STARTED.md)**: Quick start guide - **[PERFORMANCE_TUNING.md](PERFORMANCE_TUNING.md)**: Performance optimisation - **[MILESTONES.md](MILESTONES.md)**: Development roadmap diff --git a/USAGE_EXAMPLES.md b/docs/USAGE_EXAMPLES.md similarity index 99% rename from USAGE_EXAMPLES.md rename to docs/USAGE_EXAMPLES.md index 7909dec..0daab27 100644 --- a/USAGE_EXAMPLES.md +++ b/docs/USAGE_EXAMPLES.md @@ -1551,7 +1551,7 @@ let frameWidth = 2048, frameHeight = 2048 ### Documentation Resources -- [README.md](README.md) - Project overview and features +- [README.md](../README.md) - Project overview and features - [GETTING_STARTED.md](GETTING_STARTED.md) - Quick start guide - [PERFORMANCE_TUNING.md](PERFORMANCE_TUNING.md) - Performance optimisation - [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Common issues and solutions @@ -1581,7 +1581,7 @@ swift test --filter JPEGLSPerformanceBenchmarks ### Contributing -See [Copilot Instructions](.github/copilot-instructions.md) for coding guidelines and contribution requirements. +See [Copilot Instructions](../.github/copilot-instructions.md) for coding guidelines and contribution requirements. ### Related Documentation diff --git a/VERSIONING.md b/docs/VERSIONING.md similarity index 100% rename from VERSIONING.md rename to docs/VERSIONING.md diff --git a/VULKAN_GPU_ACCELERATION.md b/docs/VULKAN_GPU_ACCELERATION.md similarity index 100% rename from VULKAN_GPU_ACCELERATION.md rename to docs/VULKAN_GPU_ACCELERATION.md diff --git a/X86_64_REMOVAL_GUIDE.md b/docs/X86_64_REMOVAL_GUIDE.md similarity index 100% rename from X86_64_REMOVAL_GUIDE.md rename to docs/X86_64_REMOVAL_GUIDE.md diff --git a/jpegls.1 b/man/jpegls.1 similarity index 100% rename from jpegls.1 rename to man/jpegls.1